@lotics/cli 0.234.0 → 0.236.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.
@@ -370,6 +370,8 @@ export interface ScaffoldWorkspaceRequest {
370
370
  }>>;
371
371
  /** Bind an entity whose label already names a table here. Absent, a colliding label is refused. */
372
372
  adopt?: boolean;
373
+ /** The account each connection alias pushes through, by `cac_` id — where more than one of its provider is usable. */
374
+ connections?: Record<string, string>;
373
375
  }
374
376
  export interface ScaffoldWorkspaceResult {
375
377
  /** Every entity the model declares, in contract order. */
@@ -421,6 +423,30 @@ export interface ScaffoldWorkspaceResult {
421
423
  * whose rule nobody named is the one a reader has to be told about.
422
424
  */
423
425
  carried_rules?: string[];
426
+ /**
427
+ * Every table automation the model declares, with the table workflow it is:
428
+ * created by this run, or rewritten in place — found through the binding
429
+ * (`id`) or by its label on its table. Absent from an instance too old to
430
+ * provision one, which also refuses a model that declares any.
431
+ */
432
+ automations?: Array<{
433
+ alias: string;
434
+ entity: string;
435
+ table_workflow_id: string;
436
+ bound_by: "created" | "id" | "label";
437
+ }>;
438
+ /**
439
+ * The account each connection the model declares pushes through: named by the
440
+ * caller (`chosen`), kept from the binding this workspace held (`id`), or the
441
+ * only account of its provider the applier can use (`only`). Absent from an
442
+ * instance too old to bind one, which also refuses a model that declares any.
443
+ */
444
+ connections?: Array<{
445
+ alias: string;
446
+ connected_account_id: string;
447
+ provider: string;
448
+ bound_by: "chosen" | "id" | "only";
449
+ }>;
424
450
  /**
425
451
  * Options of an ADOPTED field that kept a mark other than the model's, by
426
452
  * binding key — every surface draws the live one. Absent from an instance too
@@ -489,10 +515,16 @@ export interface ModelBinding {
489
515
  entities: Record<string, BoundEntity>;
490
516
  templates: Record<string, BoundTarget>;
491
517
  roles: Record<string, BoundTarget>;
518
+ /** Automation alias → the table workflow it became. Absent from an instance too old to bind one. */
519
+ automations?: Record<string, BoundTarget>;
492
520
  /** `<entity-alias>:<row-ref>` → the record that first row became. */
493
521
  rows: Record<string, string>;
494
522
  /** The path a row attached a document by → the file it was uploaded as. */
495
523
  documents: Record<string, string>;
524
+ /** A plan's app alias → the app created for it here. Absent from an instance too old to bind one. */
525
+ apps?: Record<string, BoundTarget>;
526
+ /** A connection alias → the connected account an apply chose for it. Absent from an instance too old to bind one. */
527
+ connections?: Record<string, BoundTarget>;
496
528
  }
497
529
  /**
498
530
  * A workspace read BACK as a model — the inverse of the scaffold above.
@@ -860,6 +892,14 @@ export declare class LoticsClient {
860
892
  recordModelDocuments(documents: Record<string, string>): Promise<{
861
893
  recorded: number;
862
894
  }>;
895
+ /**
896
+ * Record which app each of a plan's app aliases became — stated by the create
897
+ * that made it, since an app row is made from a name and the alias is the
898
+ * author's file. What a sibling act naming that app resolves through.
899
+ */
900
+ recordModelApps(apps: Record<string, string>): Promise<{
901
+ recorded: number;
902
+ }>;
863
903
  /** Renames (and re-settings) the CURRENT workspace — the endpoint reads the
864
904
  * target from the request's workspace, never a path id. */
865
905
  updateWorkspace(body: {
@@ -1088,6 +1128,8 @@ export declare class LoticsClient {
1088
1128
  label?: string;
1089
1129
  fields?: Record<string, string>;
1090
1130
  }>;
1131
+ /** The copier's own account each connection alias pushes through, by `cac_` id. */
1132
+ connections?: Record<string, string>;
1091
1133
  }): Promise<{
1092
1134
  /** Each app's deploy, in contract order. `error` set and `deployed` null when one did not land. */
1093
1135
  apps: Array<{
@@ -1135,6 +1177,7 @@ export declare class LoticsClient {
1135
1177
  * promises, snapshotting the broken contract as a new version. */
1136
1178
  opts?: {
1137
1179
  acknowledge_breaking_api_change?: boolean;
1180
+ connections?: Record<string, string>;
1138
1181
  }): Promise<AppUpgradeResult>;
1139
1182
  /**
1140
1183
  * Capture live records from this workspace as a starter's sample data.
@@ -447,6 +447,14 @@ var LoticsClient = class {
447
447
  async recordModelDocuments(documents) {
448
448
  return this.request("PUT", "/v1/workspaces/model/binding/documents", { documents });
449
449
  }
450
+ /**
451
+ * Record which app each of a plan's app aliases became — stated by the create
452
+ * that made it, since an app row is made from a name and the alias is the
453
+ * author's file. What a sibling act naming that app resolves through.
454
+ */
455
+ async recordModelApps(apps) {
456
+ return this.request("PUT", "/v1/workspaces/model/binding/apps", { apps });
457
+ }
450
458
  /** Renames (and re-settings) the CURRENT workspace — the endpoint reads the
451
459
  * target from the request's workspace, never a path id. */
452
460
  async updateWorkspace(body) {
@@ -594,11 +602,10 @@ var LoticsClient = class {
594
602
  * refusals, both before any write. Admin-only.
595
603
  */
596
604
  async upgradeApp(app_id, opts = {}) {
597
- return this.request(
598
- "POST",
599
- `/v1/apps/${encodeURIComponent(app_id)}/upgrade`,
600
- opts.acknowledge_breaking_api_change ? { acknowledge_breaking_api_change: true } : {}
601
- );
605
+ return this.request("POST", `/v1/apps/${encodeURIComponent(app_id)}/upgrade`, {
606
+ ...opts.acknowledge_breaking_api_change ? { acknowledge_breaking_api_change: true } : {},
607
+ ...opts.connections === void 0 ? {} : { connections: opts.connections }
608
+ });
602
609
  }
603
610
  /**
604
611
  * Capture live records from this workspace as a starter's sample data.
@@ -13,30 +13,43 @@ using one to find out whether something works is the slowest possible way to lea
13
13
  ## 1 — Scaffold, or pull what exists
14
14
 
15
15
  ```
16
+ lotics app preview model.json#<app> --shots shots/ # the plan rendered over its rows: no workspace
16
17
  lotics app create "<name>" # new: a real Vite + React + TS project, deps installed
17
- lotics app create "<name>" --from model.json#<app> # new, from a checked plan: one screen per entry
18
+ lotics app create "<name>" --from model.json#<app> # new, from a checked plan: app.json, rendered
18
19
  lotics app create "<name>" --api # new, no screens: its declarations are the whole surface
19
- cd <dir> && lotics app regenerate # existing + generated: fold a later generation into it
20
+ cd <dir> && lotics app regenerate # existing + generated: rewrite its spec from the plan
20
21
  cd <dir> && lotics app pull <app_id> # existing: refresh to the latest first, then read its README
21
22
  lotics workspace build model.json # the whole plan: check, apply, then every app in it
22
23
  ```
23
24
 
24
- `lotics workspace build` is the four lines above in one, for a model that declares several apps:
25
- `scaffold check`, `scaffold apply` where this workspace differs from the model, then per app
26
- `create --from` into `<dir-of-model>/<alias-with-dashes>` when that directory does not exist and
27
- `regenerate` there when it does, then `app check` — and `app deploy` under `--deploy`. An app that
28
- conflicts or checks red is named and the rest still run, because the one you are going to fix is
29
- not a reason to leave the others in a state nobody knows. `--dry-run` writes nothing and says which
30
- apps it would create and which it would regenerate.
31
-
32
- `--from` takes the model file `lotics scaffold check` approved and the app in it, and scaffolds
33
- each screen as the registry shape over the live table its entity became — a `LifecycleDesk`, a
34
- `PartyRegister`, … from `@lotics/ui`, its slots reading the fields the plan bound. The tables
25
+ `lotics workspace build` is the create and regenerate lines above in one, for a model that
26
+ declares several apps: `scaffold check`, `scaffold apply` where this workspace differs from the
27
+ model or the model declares a table automation, then per app `create --from` into
28
+ `<dir-of-model>/<alias-with-dashes>` when that directory does not exist and `regenerate` there when
29
+ it does, then `app check` — and `app deploy` under `--deploy`. Each app is built after every sibling
30
+ it names that the run creates, since a hand-off is written off the app its alias became; otherwise
31
+ in the model's order. Two apps that name each other cannot both go second, so the one built first
32
+ is built again once the other exists, and a hand-off binds on the first run. An app that conflicts or checks
33
+ red is named and the rest still run, because the one you are going to fix is not a reason to leave
34
+ the others in a state nobody knows. `--dry-run` writes nothing and says which apps it would create
35
+ and which it would regenerate.
36
+
37
+ **Look at the plan before anything exists.** `lotics app preview <model.json>#<app>` binds the app
38
+ against the model's own ids, answers every read from the model's `rows`, renders every screen and
39
+ record at 1280 and 375, and runs the probes `app check --screens` runs — no workspace, no
40
+ credential, nothing created. `--shots <dir>` writes what it measured as PNGs. What a clause of the
41
+ plan draws is `lotics docs clauses`; the preview is how you see it drawn over your own rows.
42
+
43
+ `--from` takes the model file `lotics scaffold check` approved and the app in it, and writes
44
+ **`app.json`** — the bound plan: its one register with the shape and slots, the record each row
45
+ opens, the create panels and the acts, addressing the live `tbl_`/`fld_`/`opt_` ids its entities
46
+ became — beside a five-line `src/main.tsx` that mounts `@lotics/app-runtime` over it, one
47
+ `src/workflows/<alias>.ts` per write and an empty `src/components/index.ts`. The runtime draws the
48
+ spec; there is no screen source to edit (`node_modules/@lotics/app-runtime/AGENTS.md`). The tables
35
49
  have to exist (`lotics scaffold apply` first); a table or field the workspace lacks is refused by
36
- name. A `custom` screen arrives as a register of the cells its roles name, cut to what still reads
37
- at a phone's width — its slot list is a checklist of what the rows know, never a layout. The
38
- project's `README.md` is the plan in prose: the app, its screens with their routes and shapes, and
39
- the tables behind them.
50
+ name. The app is recorded against its plan alias, so a second `--from` for the same alias is
51
+ refused and a sibling's hand-off to it opens this app. The project's `README.md` is the plan in
52
+ prose: the app, its register and record, and the tables behind them.
40
53
 
41
54
  `--api` scaffolds the app something OUTSIDE Lotics calls (§ 9): the manifest, `src/workflows/`, a
42
55
  CI job and the two briefs, with no `index.html`, no `src/App.tsx`, no Vite config and no kit. Its
@@ -56,14 +69,13 @@ arrives fully editable rather than as an archive you have to reconstruct.
56
69
 
57
70
  **The plan changes after the app is built, and `lotics app regenerate` is how it lands.** Run it
58
71
  inside the app; the plan is the one `package.json#lotics.plan` remembers, unless `--from` names
59
- another. Each generation leaves a copy of what it wrote under `.lotics/generated/`, so the next one
60
- is a three-way merge: a generated file you have not touched is overwritten, one you have edited
61
- keeps your edit and takes the generator's, and a clash writes conflict markers, names the file and
62
- ends non-zero once every other file is folded. A file you wrote yourself is never touched; one the
63
- generator retired goes only if you left it alone. Every file it leaves as yours is named with
64
- the reason, so the next author can tell a hand dialog from a generated one. The manifest is reconciled
65
- by the same rule — the generator's aliases are replaced, retired ones removed, yours kept — then
66
- codegen and `app check` run.
72
+ another. The generator owns what it emits: `app.json` is derived from the plan and rewritten
73
+ whole, and a workflow body is rewritten too, with the text it replaced parked in
74
+ `.lotics/regenerate-dropped.patch`. `src/components/` is the one exception — seeded where it is
75
+ absent, never rewritten — because it is the hatch: `lotics app eject <screen|section|act>` hands
76
+ ONE part of the spec to a component there, and a regeneration carries that hand-over onto the new
77
+ spec. The manifest is reconciled by ownership — the generator's aliases replaced, the ones it
78
+ retired removed, the ones you added kept — then codegen and `app check` run.
67
79
 
68
80
  **Nothing is pushed.** What the live app RUNS changes at `lotics app deploy` and nowhere else: the
69
81
  bundle production serves was built against the bindings it has, so a regeneration that replaced a
@@ -71,9 +83,7 @@ live workflow body left a deployed dialog posting inputs that workflow no longer
71
83
  summary names what a deploy will add, change and leave bound instead. `--bind-new` binds the
72
84
  aliases the app does not have YET — `lotics app dev` forwards queries to production, so a new alias
73
85
  cannot be tried before something binds it — and refuses, naming them, to touch one that already
74
- exists. `--dry-run` prints the whole fold and writes nothing. The alternative
75
- is diffing two trees by hand and re-applying every edit, which is where generated apps stop being
76
- regenerated.
86
+ exists. `--dry-run` prints the whole run and writes nothing.
77
87
 
78
88
  ## 2 — Clarify what is being asked, before modelling it
79
89
 
@@ -263,35 +273,39 @@ Authoring rules for the body itself: `lotics docs workflows`.
263
273
 
264
274
  ## 7 — Screens
265
275
 
266
- **Before any JSX**, read `lotics docs ui` — the catalog and the composition grammar. A screen the
267
- plan named is a registry shape, and the shape is a kit component (`@lotics/ui/lifecycle_desk`,
268
- `@lotics/ui/party_register`, …) that owns the strip, the columns, the fit at a phone's width and
269
- the record's door: give it the rows and the slots, and swap a slot's device through its `render`
270
- rather than rebuilding the frame. For a screen no shape covers, the kit's `examples/` are whole
271
- screens as source; if the pattern is genuinely missing, build it as a kit component rather than
272
- a local one-off, or the next screen re-derives it differently.
273
-
274
- **The record half is the same deal.** A screen is `[tabs] + list → record`, and the record is one
275
- frame too: `@lotics/ui/record_page` over the section bodies the record's field roles decide. Which
276
- roles become which sections is `lotics scaffold docs` § Apps and screens; which component each
277
- section kind names is `node_modules/@lotics/ui/docs/templates.md` § The record. A drawer and a page
278
- draw the same list of sections; `lotics scaffold check` prints it per screen, and `--from` emits it.
279
- A section's ADD act rides its heading row, never inside its body.
280
-
281
- **A screen no shape fits** is declared in the plan as `"shape": "custom"` with its slots as roles
282
- (`lotics scaffold docs` § Apps and screens) — never a shape bent to fit — and built from the kit
283
- like any other. Then file `lotics report` with `wanted` opening `shape <name>`: a custom slot set
284
- that recurs becomes a shape, and the report is how the next build gets it.
285
-
286
- Two rules that cause most of the rework:
276
+ **A plan-built app's screens are its `app.json`, and `@lotics/app-runtime` draws them.** The
277
+ register, the record each row opens, its sections, the creates and the acts are all stated by the
278
+ plan, so a correction is a change to the PLAN followed by `lotics app regenerate` — or, where the
279
+ ruling is about how every app draws, a change to the runtime that reaches every app on its next
280
+ install. Every row the runtime draws — a register's subject, a list item, a card, a record's stop, a
281
+ log entry — is one anatomy read off the plan's roles (`node_modules/@lotics/app-runtime/AGENTS.md`),
282
+ and how a register is laid out is the screen's `presentation` clause. `lotics scaffold check`
283
+ prints what each screen will draw, with every value the plan left to the runtime marked
284
+ `(default)`, so the printout is where a screen is reviewed before one exists; `lotics app preview`
285
+ is where it is looked at.
286
+
287
+ **Where the plan has no word for a screen, a section or an act,** the spec names one of the app's
288
+ own components — `lotics app eject` writes it from exactly what the runtime was drawing, one part at
289
+ a time, and `lotics docs components` says what it is handed. A screen no shape fits is declared in
290
+ the plan as `"shape": "custom"` with its slots as roles (`lotics scaffold docs` § Apps and screens)
291
+ — never a shape bent to fit — and then `lotics report` with `wanted` opening `shape <name>`: a custom
292
+ slot set that recurs becomes a shape, and the report is how the next build gets it. A component
293
+ the kit lacks is built into the kit, never hand-rolled in one app.
294
+
295
+ **A hand-written app composes the kit itself.** Before any JSX, read `lotics docs ui` — the catalog
296
+ and the composition grammar; the shape components (`@lotics/ui/lifecycle_desk`,
297
+ `@lotics/ui/party_register`, …) and `@lotics/ui/record_page` own the strip, the columns, the fit at
298
+ a phone's width and the record's door, so give them rows and slots rather than rebuilding a frame.
299
+
300
+ Two rules for any code the app writes itself — a component or a hand-written screen:
287
301
 
288
302
  - **Never copy server data into `useState`.** Derive from `useQuery` / `useWorkflow` with
289
303
  `useMemo`; a copy goes stale the moment anything else writes.
290
304
  - **Design the loading, empty and error states.** Reserve their space so the layout does not jump.
291
305
 
292
- **The kit is a strong recommendation, not a requirement.** `@lotics/app-sdk` is data and RPC only —
293
- its peers are `react`, `react-dom` and `react-router`, nothing else — so an app can be plain DOM
294
- React with your own CSS and still use every hook, deploy the same way, and run the same server-side.
306
+ **The kit is a strong recommendation, not a requirement.** `@lotics/app-runtime/sdk` is data and
307
+ RPC only — its peers are `react`, `react-dom` and `react-router` — so an app can be plain DOM React
308
+ with your own CSS and still use every hook, deploy the same way, and run the same server-side.
295
309
  What the scaffold buys you is the part that is hard to get right by hand: a screen that looks
296
310
  deliberate, states that are already designed, and behaviour that matches the rest of the product.
297
311
  Building without it means owning all of that, so reach for it unless you have a specific reason not
@@ -407,8 +421,9 @@ brief travels with the app rather than living in whoever built it.
407
421
  Once scaffolded, everything below happens locally:
408
422
 
409
423
  ```
410
- edit src/workflows/<alias>.ts # or a screen, or a query
411
- lotics app regenerate --dry-run # when the PLAN moved: what the new generation would do
424
+ edit src/workflows/<alias>.ts # or a component, or a query
425
+ lotics app preview model.json#<app> --shots shots/ # when the PLAN moved: see it before a table does
426
+ lotics app regenerate --dry-run # then: what the new generation would do to this app
412
427
  lotics app codegen # after any schema change
413
428
  npm run typecheck # honest, because codegen is current
414
429
  lotics run run_app_workflow '{"app_id":…,"alias":…}' # prove the mutation path
@@ -419,14 +434,20 @@ lotics app workflow set <alias> # push the body; the server verifies
419
434
  lotics app deploy -m "…" # pushes pending bindings, then ships
420
435
  ```
421
436
 
422
- **Proving a change to `@lotics/ui` or `@lotics/app-sdk` before it is published** is
423
- `lotics app kit <path-to-that-package-in-your-checkout>`: it builds the package, packs it into this
424
- app's `.lotics/kit/`, installs it by file specifier and hashes one built file on both sides to
425
- prove the install took — a repack under the same name is otherwise served from the lockfile's first
426
- tarball, and every other signal reads as success. `app check` then says the app depends on a build
427
- that exists on one machine, and `app deploy` refuses it (the tarball is not in the source archive,
428
- so a clone could not install) unless you pass `--allow-local-kit`. `lotics app kit <path>
429
- --published` puts the registry version back.
437
+ **An app lists `@lotics/app-runtime` and nothing else of ours** — its SDK is every app's data
438
+ layer, and it depends on the kit it draws with. Where the app's own code imports the kit, it lists
439
+ `@lotics/ui` at **the runtime's range**, never its own; `lotics app kit` keeps the two in step.
440
+ **Proving a change to the runtime before it is published** is
441
+ `lotics app kit <path-to-packages/app-runtime>`: it builds the runtime, packs it into this app's
442
+ `.lotics/kit/`, installs it by file specifier and hashes one built file on both sides to prove the
443
+ install took — a repack under the same name is otherwise served from the lockfile's first tarball,
444
+ and every other signal reads as success. `app check` then says the app depends on a build that
445
+ exists on one machine, and `app deploy` refuses it (the tarball is not in the source archive, so a
446
+ clone could not install) unless you pass `--allow-local-kit`. `lotics app kit --published` puts
447
+ back the range the app listed before the first checkout — an app with none recorded moves to the
448
+ registry version — and migrates an app still on `@lotics/app-sdk`, the package the SDK was before
449
+ it moved into the runtime (`lotics docs migration`). A kit change reaches an app through the runtime that depends on
450
+ it; `LOTICS_UI_SRC` links the kit's source into `lotics app dev`.
430
451
 
431
452
  A deploy pushes any workflow body, agent prose or query that is ahead of the app **before** it
432
453
  ships the bundle, so a forgotten `set` cannot ship a bundle typed against a binding that does not
@@ -0,0 +1,129 @@
1
+ # Plan clauses
2
+
3
+ Every clause an app in a model's `apps` can state, what it draws, and an example
4
+ value. `[]` is every element of a list or a map; `as` is one form a key takes.
5
+ Each clause's rules are `lotics scaffold docs` § Apps and screens.
6
+
7
+ `lotics app preview <model.json>#<app> --shots <dir>` renders an app stating any
8
+ of them, over the rows of your own model, before a table exists.
9
+
10
+ ## On the app
11
+
12
+ | Clause | Draws | Example |
13
+ |---|---|---|
14
+ | `alias` | Stable app alias, unique within the contract | `"vans"` |
15
+ | `name` | What the app is called wherever its members open it | `"Vans"` |
16
+ | `description` | What the app is for, in a sentence shown beside its name | `"What is on the shelf, against what has to be."` |
17
+ | `icon` | A Lucide icon name, kebab-case | `"users"` |
18
+ | `theme` | The app's colour | `{"color":"sky"}` |
19
+ | `theme.color` | Theme color for the app | `"sky"` |
20
+ | `scope` | Narrow every read of this app to one row of an entity, the pick shared with every app that names it | `{"entity":"order","param":"order_id"}` |
21
+ | `reads` | "shared": this app's queries and writes carry no entity's read_scope — whoever the app is shared with reads and writes every row it draws. Absent, each read_scope binds the viewer. | `"shared"` |
22
+
23
+ ## On its screen
24
+
25
+ | Clause | Draws | Example |
26
+ |---|---|---|
27
+ | `alias` | Stable screen alias, unique within the app | `"vans"` |
28
+ | `label` | What the screen is called in the app | `"Vans"` |
29
+ | `shape` | A shape, or "custom" with its own `roles` | `"custom"` |
30
+ | `entity` | The entity whose rows this screen is over | `"van"` |
31
+ | `record` | How one record opens from the list — "drawer", "page", or "expand" to reveal it in the row; absent, the shape decides | `"page"` |
32
+ | `tabs` | The entity's lifecycle select, whose stages are the tab strip; null for none; absent, the shape decides. Any other select is a `filters` entry — except on a shape whose strip means something of its own (a reconciliation's runs, a trend's series, a worksheet's versions), which takes any select | `"run"` |
33
+ | `slots` | Slot → the field that fills it, where roles alone cannot decide; null leaves an optional slot unbound | `{"contact":"phone"}` |
34
+ | `slots[] as list of alias (at least 2)` | The tiers this slot folds by, outermost first — or the measures one level column stacks, each against its own limit | `["load","cube"]` |
35
+ | `slots[] as Quick slot` | The field AND the reader's own control for it, drawn as the column | `{"field":"hours","quick":true}` |
36
+ | `slots[].order` | A lifecycle whose stages are a WALK: the cell advances to the next one rather than offering them all | `"sequence"` |
37
+ | `roles` | For "custom" only: slot → the role that fills it | `{"identity":"identity","scan":"mark","filed":"when"}` |
38
+ | `columns` | Extra fields drawn after the slots, in this order — at most 4, each holding ONE value; never a files field, and never a field a slot already draws. `{"field", "age", "more"}` draws a lookup of a related row's category as that category's chip | `["headroom"]` |
39
+ | `columns[] as Chip column` | A related row's category drawn as ONE chip, with how long ago it was and how many others stand behind it | `{"field":"latest_kind","age":"latest_seen","more":"enquiry_count"}` |
40
+ | `columns[].age` | A date of this entity dating the row the chip's ordered lookup picks — a `latest`/`earliest` rollup of the order's date, or a lookup ordered the same way — drawn after the chip as how long ago it was | `"latest_seen"` |
41
+ | `columns[].more` | A count rollup of this entity over the SAME link — drawn beside the chip as how many OTHER rows there are | `"enquiry_count"` |
42
+ | `sources` | Other entities whose rows this register reads beside its own, in ONE read sorted by its order — each declares a field for every role the register's slots draw and, under the same alias, every other field it draws, orders or narrows by; a row opens in its own entity's record, and the create and row acts are this screen's entity's | `["consigned_item"]` |
43
+ | `create` | The fields a new row is asked for, in this order — at most 12, each a field of this entity a draft can ask for, and every field the row cannot be written without among them; absent, the draft asks for every field a person states | `["name","company","page","phone"]` |
44
+ | `writes` | false makes this screen's record read-only — no field editor, no stage advance; "children" draws the record at rest and leaves the rows it owns operable; "record" leaves the record operable and draws the rows it owns at rest; absent, both are operable | `false` |
45
+ | `acts` | The papers this register makes — from one row, from a ticked set, and from the whole view | `{"row":[{"kind":"agent","label":"Find the contact","agent":"contact_finder","fills":["phone","email"]}],"expo…` |
46
+ | `acts.row` | The acts in every row's ⋯ menu, in this order | `[{"kind":"sibling","label":"Open the order","app":"orders","record":"order"}]` |
47
+ | `acts.row[] as Agent act` | A run of an app agent over the row, whose proposed values the reader reviews before one write Also `acts.record[] as Agent act`, `acts.selection[] as Agent act`, `section_acts[][] as Agent act`. | `{"kind":"agent","label":"Find the contact","agent":"contact_finder","fills":["phone","email"]}` |
48
+ | `acts.row[].place` | Draw this act ON the row rather than in its ⋯ menu — at most one per screen | `"cta"` |
49
+ | `acts.row[].when` | Offer this act only while the row stands at one of these stages | `{"field":"state","in":["ready"]}` |
50
+ | `acts.row[].needs` | Fields of this entity — the act is offered only while at least one of them holds a value | `["name"]` |
51
+ | `acts.row[].confirm` | Ask before this act runs, with its own label as the commit word — for a press that cannot be taken back | `true` |
52
+ | `acts.row[] as Workflow act` | The row handed to a workflow the app binds, run on the press Also `acts.record[] as Workflow act`, `acts.selection[] as Workflow act`, `section_acts[][] as Workflow act`. | `{"kind":"workflow","label":"Hand to despatch","workflow":"hand_to_despatch","inputs":{"order_id":"record"},"w…` |
53
+ | `acts.row[].asks` | What the reader states in the act's panel before it runs: the workflow's own input name → the field whose editor asks it | `{"owner":"owner"}` |
54
+ | `acts.row[].moves` | The stage of this entity's lifecycle the body lands the row at — the act's alone: no stage picker, ladder or board of this app moves a row there | `"converted"` |
55
+ | `acts.row[] as Sibling act` | A record of this row opened in a sibling app of this plan, in place Also `acts.record[] as Sibling act`, `acts.selection[] as Sibling act`, `section_acts[][] as Sibling act`. | `{"kind":"sibling","label":"Open the order","app":"orders","record":"order"}` |
56
+ | `acts.row[] as Recording act` | A call, a visit, a walk-through recorded onto the row and filed through a workflow once transcribed Also `acts.record[] as Recording act`, `acts.selection[] as Recording act`, `section_acts[][] as Recording act`. | `{"kind":"recording","label":"Record a call","workflow":"log_call","inputs":{"prospect_id":"record"}}` |
57
+ | `acts.row[] as Paper act` | A paper made from a template this model declares — from the row, or from every ticked row at once Also `acts.record[] as Paper act`, `acts.selection[] as Paper act`, `section_acts[][] as Paper act`. | `{"label":"Chase the papers","template":"chaser"}` |
58
+ | `acts.record` | The acts in the RECORD's own header menu, in this order — the same reach as a row's, with the work open | `[{"label":"Print the order file","template":"order_file"}]` |
59
+ | `acts.selection` | The acts over the TICKED rows — each a `template`, generating ONE document over the set | `[{"label":"Remittance advice","template":"remittance"}]` |
60
+ | `acts.export` | Save the rows in view — true for a workbook of the drawn columns, or a template this model declares | `true` |
61
+ | `acts.export as Export template` | The rows in view as a paper laid out by the trade's own template | `{"template":"customer_book"}` |
62
+ | `acts.import` | Turn a file into rows — mapped, validated per row, previewed, then upserted by the entity's natural key | `{"kind":"import","label":"Import the statement","entity":"payment","key":"reference","columns":["date","amoun…` |
63
+ | `acts.import.columns` | The fields the file may fill, in this order — absent, every field a person states | `["date","amount","direction","state"]` |
64
+ | `section_acts` | Acts on the RECORD's own sections, keyed by the alias each section is derived from — a field alias (its progress, its prose, its set, its charge, its files) or a child entity's alias (its register, its desk, its run, its log) | `{"document":[{"label":"Chase the papers","template":"chaser"}]}` |
65
+ | `sections` | How a section of the RECORD is DRAWN, keyed by the child entity alias its register is derived from — or `{"heading": …}`, `{"facts": […]}` or both alone to rename any section of the rows it owns or head it with the record's facts | `{"order_line":{"draw":"worksheet","cost":"line_cost","sell":"line_total"}}` |
66
+ | `sections[] as Worksheet section` | Priced lines the reader works down in place, each part footed and the sheet closing under them | `{"draw":"worksheet","cost":"line_cost","sell":"line_total"}` |
67
+ | `sections[].heading` | What this section is headed on the record; absent, the child entity's own label | `"Touches"` |
68
+ | `sections[].facts` | Fields of THIS record drawn at the head of the section, above its rows — filed here, and so in no band of the record's facts | `["outstanding"]` |
69
+ | `sections[].cost` | The child's field holding what a line COSTS — the base the margin is taken against | `"line_cost"` |
70
+ | `sections[].sell` | The child's field holding what it SELLS for — the figure the sheet is read for | `"line_total"` |
71
+ | `sections[] as Ledger section` | A book of movements — closes on its total, read against the record's rollup that sums it | `{"draw":"ledger","facts":["outstanding"]}` |
72
+ | `sections[] as Itinerary section` | A run of stops read a day at a time — stated to place the child's fields on the stop where the roles' placement is not the reading | `{"draw":"itinerary","lines":[["window","fitting_hours","buyer"],["line_total"]],"badge":"status"}` |
73
+ | `sections[].name` | The child's field that NAMES the entry and leads it; absent, its `identity` | `"subject"` |
74
+ | `sections[].words` | The child's text field the entry SAYS; absent, its `body`, else (on a log) its first markdown text | `"words"` |
75
+ | `sections[].lines` | The entry's supporting lines, at most 2, each the child's fields it reads in order, at most 4; absent, a log reads its selects, the parties it was with and the address it reached them at, and a stop reads its slot and its party, with its amount on a second line | `[["with_party"]]` |
76
+ | `sections[].badge` | The child's select drawn as the entry's status at the right — a lifecycle or a category; absent, a stop's lifecycle, and nothing on a log | `"status"` |
77
+ | `sections[].figure` | The child's number worn at the trailing end of the entry's head; absent, a log's amount, and nothing on a stop | `"hours_open"` |
78
+ | `sections[].evidence` | The child's fields drawn under the words as what the entry shows, a playable file played; absent, its `recording` | `["photos"]` |
79
+ | `sections[].detail` | The child's fields kept behind one fold under the words; absent, its `verbatim` | `["desk_note"]` |
80
+ | `sections[] as Claim section` | Lines claimed against a priced schedule, one period at a time — the contract, before, now, to date and what is left | `{"draw":"claim","quantity":"now","contract":"contract_qty","price":"price","previous":"before","unit":"unit",…` |
81
+ | `sections[].unit` | The child's field naming what the line is counted in | `"unit"` |
82
+ | `sections[].retention` | A percentage field of THIS record — the share held back from what is claimed | `"retention"` |
83
+ | `sections[] as Tree section` | The rows nested under each other, each money figure summed up the tree | `{"draw":"tree","nest":"part_of"}` |
84
+ | `sections[] as Gantt section` | The rows as bars across the calendar — from each row's `when`, for the days its measure counts | `{"draw":"gantt","until":"finishes","nest":"part_of","depends_on":"after","baseline":{"start":"planned_start",…` |
85
+ | `sections[].until` | The child's date field each bar is drawn TO; absent, a bar runs for the days its measure counts | `"finishes"` |
86
+ | `sections[].nest` | The child's one-row link to its own entity — folds a row's bars under the row it sits under | `"part_of"` |
87
+ | `sections[].depends_on` | The child's link to its own entity naming the rows that must finish before a row starts | `"after"` |
88
+ | `sections[].baseline` | The child's date fields each row was PLANNED to start and end on, drawn under its bar | `{"start":"planned_start","end":"planned_end"}` |
89
+ | `sections[] as Curve section` | Two figures of the rows, each added up to date along the rows' `when` and drawn as two lines | `{"draw":"curve","planned":"planned_value","actual":"earned_value"}` |
90
+ | `sections[] as Log section` | Dated entries read in the words each states — stated to place the child's fields on the entry where the roles' placement is not the reading | `{"draw":"log","name":"subject","words":"message","evidence":["photos"],"detail":["desk_note"],"lines":[["with…` |
91
+ | `sections[] as Publish section` | One row per destination this record stands on — the strip, the preview, and the press that sends them | `{"draw":"publish","states":{"queued":"queued","published":"out","failed":"refused","by_hand":"by_hand"},"text…` |
92
+ | `sections[].text` | A plain text on the child — what this one destination goes out with instead of the body | `"wording"` |
93
+ | `sections[].permalink` | A link-formatted text on the child — where the post landed | `"address"` |
94
+ | `sections[].error` | A text on the child holding the platform's own refusal | `"fault"` |
95
+ | `sections[].link` | A link-formatted text of THIS entity — the address the post carries as its card | `"source"` |
96
+ | `sections[].when` | Offer the desk's Publish only while THIS record stands at one of these stages | `{"field":"state","in":["ready"]}` |
97
+ | `sections[] as Acts section` | Acts waiting for a person — the words, where to reach them, and the sheet that records what came of it | `{"draw":"acts","heading":"Next steps","states":{"queued":"waiting","done":"done","skipped":"passed"},"verb":"…` |
98
+ | `sections[].why` | A text on the child — why this is worth doing now | `"why_now"` |
99
+ | `sections[].reason` | A text or a single select on the child — why it was passed over | `"pass_reason"` |
100
+ | `sections[].reach` | Verb option → where that act is done; a verb this map leaves out is done wherever the reader already is | `{"reply":{"at":"enquiry.source","by":"link"},"message":{"at":"page","by":"link"},"call":{"at":"phone","by":"p…` |
101
+ | `sections[].withhold` | A yes/no of THIS entity — while it reads yes, no act reaches out, and the reach verb says why | `"quiet"` |
102
+ | `sections[].cta` | The register's row wears this record's NEXT waiting act as its verb — the one press that reaches the person | `true` |
103
+ | `sections[] as Section heading` | Rename a section of the rows this record owns, or head it with the record's facts that frame them, however the roles draw it | `{"heading":"Touches"}` |
104
+ | `children` | The child entities whose rows the RECORD draws in this app, in this order — only these; absent, every one its links derive | `["works_task","claim_line"]` |
105
+ | `filters` | The chips beside the search — a single-select or select_member field's alias, `{"field": …, "default": "mine"}` to open a member field on the reader's own rows, or a lens this model states as predicates over the entity's fields | `["kind"]` |
106
+ | `filters[] as Lens` | A lens the MODEL states as predicates over the entity's fields, where no select holds the answer | `{"label":"Shelf","predicates":[{"label":"Empty","tone":"rose","where":{"node_type":"group","logic":"and","chi…` |
107
+ | `filters[].sort` | The order the rows are read in while the lens is on | `{"field":"next_due","order":"asc"}` |
108
+ | `filters[].sort.order` | "asc" (the default) puts the soonest or smallest first; a row holding none goes last | `"asc"` |
109
+ | `filters[] as Member lens` | A member field's lens, opened on the reader's own rows | `{"field":"handled_by","default":"mine"}` |
110
+ | `facts` | How the record's facts are BANDED — where the aside's fields group under captions | `{"groups":[{"caption":"The movement","fields":["date","direction"]},{"caption":"Where it was banked","fields"…` |
111
+ | `ladder` | The flow drawn as a ladder of dated rungs, for a long flow whose path the reader needs to see; absent, the stage is the record header's status | `true` |
112
+ | `summary` | What the rows in view come to, stated before or after their names | `{"above":"counts"}` |
113
+ | `summary.totals` | Number fields the whole view adds up — under the column that draws one, else in a band beneath the register | `["lines"]` |
114
+ | `summary.above` | A band of figures over the register: "counts" for how many rows and how many need attention, or number fields — each added up over the window, or `{"field": …, "at": "end"}` for a stock STANDING at its end | `"counts"` |
115
+ | `summary.above[] as Stock` | A stock standing at the end of the window, beside the sums over it | `{"field":"in_yard","at":"end"}` |
116
+ | `summary.ageing` | How old the money over this register is — the amount split by how many days past its date each row is | `{"amount":"outstanding","due":"due_date","buckets":[14,30,60]}` |
117
+ | `summary.ageing.buckets` | The bucket edges, in days past due, ascending — absent, 30, 60, 90 | `[14,30,60]` |
118
+ | `summary.trend` | Which way this figure moved across the window the reader is reading — needs a `period` to be a window of | `{"field":"amount","direction":"up"}` |
119
+ | `summary.curve` | Planned against actual, each added up to date over the rows' dates and drawn as two lines — read along the `period`, else the `when` | `{"planned":"planned_value","actual":"earned_value"}` |
120
+ | `period` | A date field on the entity the reader narrows the view by; absent, the view is every row | `"day"` |
121
+ | `presentation` | How this screen and the record it opens are DRAWN, where the shape's own answer is not the one wanted | `{"lead":"none"}` |
122
+ | `presentation.lead` | What each row leads with; absent, what the shape's rows are decides | `"none"` |
123
+ | `presentation.density` | How many lines a row's subject may take; absent, what the shape's rows are decides | `"dense"` |
124
+ | `presentation.layout` | How the rows are arranged; absent, a table | `"gantt"` |
125
+ | `presentation.line` | The register row's supporting line, in this order — at most 2 fields of this entity, each a plain text or a single select; absent, the key the row is filed under | `["company","role"]` |
126
+ | `presentation.until` | For a gantt: the date field each bar is drawn TO; absent, the bar runs for the days its measure counts | `"due_back"` |
127
+ | `presentation.nest` | A one-row link from this entity to itself — the row each row sits under; affords `layout: "tree"` and nests a gantt's bars | `"part_of"` |
128
+ | `presentation.depends_on` | For a gantt: a link from this entity to itself naming the rows that must finish before a row starts | `"after"` |
129
+ | `presentation.baseline` | For a gantt: the date fields the row was PLANNED to start and end on, drawn under its bar | `{"start":"planned_start","end":"planned_end"}` |