@lotics/cli 0.233.0 → 0.235.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +3 -2
- package/README.md +22 -12
- package/dist/src/cli.js +5323 -2739
- package/dist/src/client.d.ts +71 -0
- package/dist/src/client.js +12 -5
- package/docs/building_an_app.md +83 -62
- package/docs/clauses.md +128 -0
- package/docs/cli_reference.md +18 -17
- package/docs/migration.md +25 -2
- package/package.json +2 -1
package/dist/src/client.d.ts
CHANGED
|
@@ -344,6 +344,15 @@ export interface KnowledgeWarnings {
|
|
|
344
344
|
/** `knowledge_expects` doc names with no matching workspace doc. */
|
|
345
345
|
missing_expected_docs: string[];
|
|
346
346
|
}
|
|
347
|
+
/**
|
|
348
|
+
* An option's mark as stored and read back — `storedOptionMarkSchema` in
|
|
349
|
+
* `@lotics/shared`, typed structurally here because a published `.d.ts` cannot
|
|
350
|
+
* resolve that specifier.
|
|
351
|
+
*/
|
|
352
|
+
export interface OptionMark {
|
|
353
|
+
kind: string;
|
|
354
|
+
name: string;
|
|
355
|
+
}
|
|
347
356
|
/**
|
|
348
357
|
* A workspace MODEL on the wire — a package contract with no apps, plus the
|
|
349
358
|
* first rows that travel beside it keyed by entity alias.
|
|
@@ -361,6 +370,8 @@ export interface ScaffoldWorkspaceRequest {
|
|
|
361
370
|
}>>;
|
|
362
371
|
/** Bind an entity whose label already names a table here. Absent, a colliding label is refused. */
|
|
363
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>;
|
|
364
375
|
}
|
|
365
376
|
export interface ScaffoldWorkspaceResult {
|
|
366
377
|
/** Every entity the model declares, in contract order. */
|
|
@@ -412,6 +423,49 @@ export interface ScaffoldWorkspaceResult {
|
|
|
412
423
|
* whose rule nobody named is the one a reader has to be told about.
|
|
413
424
|
*/
|
|
414
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
|
+
}>;
|
|
450
|
+
/**
|
|
451
|
+
* Options of an ADOPTED field that kept a mark other than the model's, by
|
|
452
|
+
* binding key — every surface draws the live one. Absent from an instance too
|
|
453
|
+
* old to state it, for the reason `carried_rules` is.
|
|
454
|
+
*/
|
|
455
|
+
kept_marks?: Array<{
|
|
456
|
+
alias: string;
|
|
457
|
+
model: OptionMark;
|
|
458
|
+
live: OptionMark;
|
|
459
|
+
}>;
|
|
460
|
+
/**
|
|
461
|
+
* ADOPTED fields the model marks whose marks the run did not write, by binding
|
|
462
|
+
* key, with the live options that wear none and that the model does not mark.
|
|
463
|
+
* Absent from an instance too old to state it, for the reason `carried_rules` is.
|
|
464
|
+
*/
|
|
465
|
+
unwritten_marks?: Array<{
|
|
466
|
+
alias: string;
|
|
467
|
+
bare: string[];
|
|
468
|
+
}>;
|
|
415
469
|
/**
|
|
416
470
|
* Bound aliases this workspace calls something other than the model does.
|
|
417
471
|
*
|
|
@@ -461,10 +515,16 @@ export interface ModelBinding {
|
|
|
461
515
|
entities: Record<string, BoundEntity>;
|
|
462
516
|
templates: Record<string, BoundTarget>;
|
|
463
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>;
|
|
464
520
|
/** `<entity-alias>:<row-ref>` → the record that first row became. */
|
|
465
521
|
rows: Record<string, string>;
|
|
466
522
|
/** The path a row attached a document by → the file it was uploaded as. */
|
|
467
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>;
|
|
468
528
|
}
|
|
469
529
|
/**
|
|
470
530
|
* A workspace read BACK as a model — the inverse of the scaffold above.
|
|
@@ -832,6 +892,14 @@ export declare class LoticsClient {
|
|
|
832
892
|
recordModelDocuments(documents: Record<string, string>): Promise<{
|
|
833
893
|
recorded: number;
|
|
834
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
|
+
}>;
|
|
835
903
|
/** Renames (and re-settings) the CURRENT workspace — the endpoint reads the
|
|
836
904
|
* target from the request's workspace, never a path id. */
|
|
837
905
|
updateWorkspace(body: {
|
|
@@ -1060,6 +1128,8 @@ export declare class LoticsClient {
|
|
|
1060
1128
|
label?: string;
|
|
1061
1129
|
fields?: Record<string, string>;
|
|
1062
1130
|
}>;
|
|
1131
|
+
/** The copier's own account each connection alias pushes through, by `cac_` id. */
|
|
1132
|
+
connections?: Record<string, string>;
|
|
1063
1133
|
}): Promise<{
|
|
1064
1134
|
/** Each app's deploy, in contract order. `error` set and `deployed` null when one did not land. */
|
|
1065
1135
|
apps: Array<{
|
|
@@ -1107,6 +1177,7 @@ export declare class LoticsClient {
|
|
|
1107
1177
|
* promises, snapshotting the broken contract as a new version. */
|
|
1108
1178
|
opts?: {
|
|
1109
1179
|
acknowledge_breaking_api_change?: boolean;
|
|
1180
|
+
connections?: Record<string, string>;
|
|
1110
1181
|
}): Promise<AppUpgradeResult>;
|
|
1111
1182
|
/**
|
|
1112
1183
|
* Capture live records from this workspace as a starter's sample data.
|
package/dist/src/client.js
CHANGED
|
@@ -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
|
-
|
|
599
|
-
|
|
600
|
-
|
|
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.
|
package/docs/building_an_app.md
CHANGED
|
@@ -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:
|
|
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:
|
|
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
|
|
25
|
-
`scaffold check`, `scaffold apply` where this workspace differs from the
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
apps
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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.
|
|
37
|
-
|
|
38
|
-
|
|
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.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
|
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
|
-
**
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
a
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
|
293
|
-
its peers are `react`, `react-dom` and `react-router
|
|
294
|
-
|
|
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
|
|
411
|
-
lotics app
|
|
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
|
-
**
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
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
|
package/docs/clauses.md
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
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
|
+
| `summary` | What the rows in view come to, stated before or after their names | `{"above":"counts"}` |
|
|
112
|
+
| `summary.totals` | Number fields the whole view adds up — under the column that draws one, else in a band beneath the register | `["lines"]` |
|
|
113
|
+
| `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"` |
|
|
114
|
+
| `summary.above[] as Stock` | A stock standing at the end of the window, beside the sums over it | `{"field":"in_yard","at":"end"}` |
|
|
115
|
+
| `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]}` |
|
|
116
|
+
| `summary.ageing.buckets` | The bucket edges, in days past due, ascending — absent, 30, 60, 90 | `[14,30,60]` |
|
|
117
|
+
| `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"}` |
|
|
118
|
+
| `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"}` |
|
|
119
|
+
| `period` | A date field on the entity the reader narrows the view by; absent, the view is every row | `"day"` |
|
|
120
|
+
| `presentation` | How this screen and the record it opens are DRAWN, where the shape's own answer is not the one wanted | `{"lead":"none"}` |
|
|
121
|
+
| `presentation.lead` | What each row leads with; absent, what the shape's rows are decides | `"none"` |
|
|
122
|
+
| `presentation.density` | How many lines a row's subject may take; absent, what the shape's rows are decides | `"dense"` |
|
|
123
|
+
| `presentation.layout` | How the rows are arranged; absent, a table | `"gantt"` |
|
|
124
|
+
| `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"]` |
|
|
125
|
+
| `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"` |
|
|
126
|
+
| `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"` |
|
|
127
|
+
| `presentation.depends_on` | For a gantt: a link from this entity to itself naming the rows that must finish before a row starts | `"after"` |
|
|
128
|
+
| `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"}` |
|