@lotics/cli 0.306.1 → 0.307.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/cli.js +631 -89
- package/docs/agent_surface.md +254 -0
- package/docs/building_an_app.md +14 -8
- package/docs/cli_reference.md +2 -1
- package/docs/design.md +14 -5
- package/docs/workflow_steps.md +151 -0
- package/docs/workflows.md +190 -20
- package/package.json +2 -1
package/docs/workflows.md
CHANGED
|
@@ -291,36 +291,206 @@ row of an `.xlsx`, `.xls` or `.csv` sheet in one call, each an object keyed by t
|
|
|
291
291
|
A sheet past what one call reads is refused with its size, never cut short: the step's entry in
|
|
292
292
|
`workflow_steps` states the bounds.
|
|
293
293
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
294
|
+
`import_rows`, a workflow step, matches those rows to a table by a key and writes what differs. `match`
|
|
295
|
+
names the fields that identify a row, compared as trimmed text with case ignored: a row matching no live
|
|
296
|
+
row is added, one matching a row changes only the cells that differ, and one that changes nothing is
|
|
297
|
+
counted and not written, so the same file run twice writes nothing the second time. `values` holds a
|
|
298
|
+
row's cells by field key, as `create_records` takes them; a key the row omits is left as the table holds
|
|
299
|
+
it, and `null` is an empty cell, which differs from a filled one as any other value does.
|
|
300
|
+
|
|
301
|
+
A `match` field may also be a link field holding one row, compared by the row linked: lines keyed by
|
|
302
|
+
their parent and a line number state `match: ["fld_invoice", "fld_line"]`, the parent given as its
|
|
303
|
+
record id or as `{ lookup: "<text>" }`.
|
|
304
|
+
|
|
305
|
+
What the step is not certain of is held — answered with why, and not written: a row carrying `hold`
|
|
306
|
+
(its text is the message); a row whose key several live rows carry, or an earlier row of the same call;
|
|
307
|
+
a row the table's checks refuse, or whose row is locked; with `create: false`, a row matching none. A
|
|
308
|
+
held row never fails the step, and the rows beside it are written. A row whose key names a row outside
|
|
309
|
+
the rows the run may write is held `refused` with nothing of that row: no `record_id`, no `changes`.
|
|
310
|
+
|
|
311
|
+
The step removes no row, and a row the source leaves out is never touched. A row carrying
|
|
312
|
+
`remove: true`, with its key in `values`, proposes moving the row its key names to the trash: it is
|
|
313
|
+
always held, whatever `create` and `conflicts` say, and a key naming no row is counted unchanged.
|
|
314
|
+
|
|
315
|
+
`conflicts` states what a matched row does where a live cell holds another value: `"hold"` writes the
|
|
316
|
+
row's empty cells and holds the differing ones, `"overwrite"` writes over them, `"fill_empty"` leaves
|
|
317
|
+
them. `conflicts.fields` gives one field its own. A number and the field its unit or currency is read
|
|
318
|
+
from on each row are one cell, under the number's policy: both are written, or both held.
|
|
319
|
+
|
|
320
|
+
A link cell takes `{ lookup: "<text>" }` in place of record ids: the linked row is the one row holding
|
|
321
|
+
that text in the linked table's unique or autonumber field, else the one titled it, among the rows the
|
|
322
|
+
run reads of that table. A text naming no row, or several, holds the row, and the rows it could be are
|
|
323
|
+
answered beside it. A link holding many rows takes a list, `[{ lookup: "<text>" }, …]`: each text is
|
|
324
|
+
resolved by itself.
|
|
325
|
+
|
|
326
|
+
The step keeps nothing of its own: its answer is everything it did. `written` lists each record it
|
|
327
|
+
wrote — a changed one with each field from old to new as text, a created one by its id alone — and
|
|
328
|
+
`held_rows` each row it held: its `kind`, `reason` and `message`, the `record_id` of the row it matched,
|
|
329
|
+
its `key`, the `changes` it would make, the `links` it could not resolve with their `candidates`, and
|
|
330
|
+
`values` — its key and the cells it would write, as the call gave them. A later call given a held row's
|
|
331
|
+
`values` under `conflicts: { default: "overwrite" }` writes it. Both lists are read under the run's own
|
|
332
|
+
reach of the table, and a workflow hands its caller only what it returns. Two calls matching one table
|
|
333
|
+
by the same fields take turns, so the second matches the rows the first added. The step's entry in
|
|
334
|
+
`workflow_steps` states its bounds.
|
|
335
|
+
|
|
336
|
+
`undo_record_changes`, a workflow step, puts back what one run wrote on records of a table. It takes the
|
|
337
|
+
table, up to 500 record ids and the run — what `runtime.execution_id` held in it. A field goes back
|
|
338
|
+
where the row still holds what the run wrote. A row the run created goes to the trash
|
|
339
|
+
where nothing else has written it or linked a row to it since; in a table where a workflow writes each
|
|
340
|
+
new row, every row created reads as written since, and is kept. A row the run moved to the trash comes
|
|
341
|
+
back where nothing has touched it since, without the links to it that its removal took off other rows.
|
|
342
|
+
A row the run brought back from the trash stays out of it. A write of the run that was itself an undo is
|
|
343
|
+
not put back. What the step leaves as it stands is answered in `kept`, one entry a record and reason:
|
|
344
|
+
something changed it since, it is outside the rows the member may write, the table refused the write,
|
|
345
|
+
what the run wrote on it cannot be read, or the run brought it back from the trash.
|
|
346
|
+
|
|
347
|
+
Two more workflow steps read and settle one record's saves: `get_record_activity`, a workflow step,
|
|
348
|
+
lists them, and `undo_record_change`, a workflow step, puts one back. What they read is the run's to
|
|
349
|
+
read; inside an app's workflow it is what the app reads — the fields of the app's entity over the table,
|
|
350
|
+
a record that entity's own read reaches. An undo, of one save or of a run's writes, is the own save of the
|
|
351
|
+
member the run acts for — whoever started the app's workflow, pressed the button, or made the write a
|
|
352
|
+
table's workflow followed: that member holds the table's write as the step runs, each row within the
|
|
353
|
+
rows they may write, and the table's locks, checks and write rules answer for them. A run that acts for
|
|
354
|
+
no member — a schedule's, a public visitor's — reads, and its undos are refused.
|
|
355
|
+
|
|
356
|
+
What a business keeps of an import — the file, its counts, the lines waiting for a person — is rows of
|
|
357
|
+
its own tables, written by the workflow that ran the step. A garden centre brings its grower's price
|
|
358
|
+
sheet into its plants, and keeps each sheet and its lines. A line names its plant through a link
|
|
359
|
+
holding many rows: one holding a single row would make the line that plant's own, and a plant its
|
|
360
|
+
lines belong to is not moved to the trash.
|
|
361
|
+
|
|
362
|
+
These tables are shown beside the plants, in the same app: the sheets as a log of the plants' register
|
|
363
|
+
(`logs`, `lotics docs model/logs`), a sheet's lines on the page it opens. A line keeping what a cell held
|
|
364
|
+
and what it holds now in two fields reads one against the other (`compare`, `lotics docs model/blocks`).
|
|
297
365
|
|
|
298
366
|
```
|
|
299
367
|
const sheet = await excel_get_rows({ // a workflow step
|
|
300
368
|
file_id: trigger.app_workflow.inputs.file,
|
|
301
|
-
columns: ["Code", "
|
|
369
|
+
columns: ["Code", "Name", "Price"],
|
|
302
370
|
});
|
|
303
371
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
372
|
+
const imported = await import_rows({ // a workflow step
|
|
373
|
+
table_id: "tbl_plants",
|
|
374
|
+
match: ["fld_code"],
|
|
375
|
+
rows: sheet.rows.map((row, at) => ({
|
|
376
|
+
line: at + 2,
|
|
377
|
+
hold: isNull(row["Price"]) ? "The sheet states no price" : null,
|
|
378
|
+
values: {
|
|
379
|
+
fld_code: toString(row["Code"]),
|
|
380
|
+
fld_name: toString(row["Name"]),
|
|
381
|
+
fld_price: toNumber(row["Price"]),
|
|
382
|
+
},
|
|
383
|
+
})),
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
const kept = await create_records({
|
|
387
|
+
table_id: "tbl_price_sheets",
|
|
388
|
+
records: [{
|
|
389
|
+
fld_file: [trigger.app_workflow.inputs.file],
|
|
390
|
+
fld_run: runtime.execution_id,
|
|
391
|
+
fld_read: imported.read,
|
|
392
|
+
fld_created: imported.created,
|
|
393
|
+
fld_changed: imported.changed,
|
|
394
|
+
fld_held: imported.held,
|
|
395
|
+
}],
|
|
396
|
+
});
|
|
397
|
+
const sheet_id = kept.record_ids[0];
|
|
398
|
+
|
|
399
|
+
for (let start = 0; start < size(imported.written); start += 50) {
|
|
400
|
+
await create_records({
|
|
401
|
+
table_id: "tbl_sheet_lines",
|
|
402
|
+
records: slice(imported.written, start, start + 50).map((one) => ({
|
|
403
|
+
fld_sheet: [sheet_id],
|
|
404
|
+
fld_plant: [one.record_id],
|
|
405
|
+
fld_line: one.line,
|
|
406
|
+
fld_outcome: one.kind,
|
|
407
|
+
fld_changes: join(one.changes.map((change) => `${change.label}: ${change.before ?? "—"} → ${change.after ?? "—"}`), "; "),
|
|
408
|
+
})),
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
for (let start = 0; start < size(imported.held_rows); start += 50) {
|
|
412
|
+
await create_records({
|
|
413
|
+
table_id: "tbl_sheet_lines",
|
|
414
|
+
records: slice(imported.held_rows, start, start + 50).map((one) => ({
|
|
415
|
+
fld_sheet: [sheet_id],
|
|
416
|
+
fld_plant: isNull(one.record_id) ? [] : [one.record_id],
|
|
417
|
+
fld_line: one.line,
|
|
418
|
+
fld_outcome: "held",
|
|
419
|
+
fld_why: one.message,
|
|
420
|
+
fld_code: one.key["fld_code"],
|
|
421
|
+
fld_name: toString(one.values["fld_name"]),
|
|
422
|
+
fld_price: toNumber(one.values["fld_price"]),
|
|
423
|
+
fld_changes: join(one.changes.map((change) => `${change.label}: ${change.before ?? "—"} → ${change.after ?? "—"}`), "; "),
|
|
424
|
+
})),
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
return({ status: "success", message: `${imported.created} added, ${imported.changed} changed, ${imported.held} held` });
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
A held line keeps the `values` the step answered for it — its key and each cell it would write — in
|
|
432
|
+
fields of its own. It is applied by the act on it, whose workflow hands the step those values again and
|
|
433
|
+
lets it write over what the plant holds. A cell the line holds none of is left out, so a held line
|
|
434
|
+
never empties a cell of the plant:
|
|
435
|
+
|
|
436
|
+
```
|
|
437
|
+
const line = await get_record({ table_id: "tbl_sheet_lines", record_id: trigger.app_workflow.inputs.record_id });
|
|
438
|
+
const values = {
|
|
439
|
+
fld_code: line.data["fld_code"],
|
|
440
|
+
...(isEmpty(line.data["fld_name"]) ? {} : { fld_name: line.data["fld_name"] }),
|
|
441
|
+
...(isNull(line.data["fld_price"]) ? {} : { fld_price: line.data["fld_price"] }),
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
const applied = await import_rows({ // a workflow step
|
|
445
|
+
table_id: "tbl_plants",
|
|
446
|
+
match: ["fld_code"],
|
|
447
|
+
conflicts: { default: "overwrite" },
|
|
448
|
+
rows: [{ values }],
|
|
449
|
+
});
|
|
450
|
+
if (applied.held > 0) {
|
|
451
|
+
return({ status: "error", message: applied.held_rows[0].message });
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
await update_records({ table_id: "tbl_sheet_lines", record_ids: [line.id], set: { fld_outcome: "applied" } });
|
|
455
|
+
return({ status: "success", message: "Applied." });
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
A sheet is undone by the act on it, whose workflow hands the step the run the sheet kept and the
|
|
459
|
+
plants its lines name, 500 at a time:
|
|
460
|
+
|
|
461
|
+
```
|
|
462
|
+
const sheet = await get_record({ table_id: "tbl_price_sheets", record_id: trigger.app_workflow.inputs.record_id });
|
|
463
|
+
|
|
464
|
+
let reverted = 0;
|
|
465
|
+
let left = 0;
|
|
466
|
+
let offset = 0;
|
|
467
|
+
let more = true;
|
|
468
|
+
while (more) {
|
|
469
|
+
const lines = await query_records({
|
|
470
|
+
table_id: "tbl_sheet_lines",
|
|
471
|
+
filters: {
|
|
472
|
+
logic: "and",
|
|
473
|
+
children: [
|
|
474
|
+
{ field_key: "fld_sheet", operator: "has_any_of", value: [sheet.id] },
|
|
475
|
+
{ field_key: "fld_outcome", operator: "is_any_of", value: ["created", "changed"] },
|
|
476
|
+
],
|
|
477
|
+
},
|
|
478
|
+
pagination: { offset, limit: 500 },
|
|
310
479
|
});
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
records: fresh.map((row) => ({
|
|
317
|
-
fld_code: toString(row["Code"]),
|
|
318
|
-
fld_filed: isString(row["Filed"]) ? row["Filed"] : null,
|
|
319
|
-
fld_amount: toNumber(row["Amount"]),
|
|
320
|
-
})),
|
|
480
|
+
if (size(lines.records) > 0) {
|
|
481
|
+
const undone = await undo_record_changes({ // a workflow step
|
|
482
|
+
table_id: "tbl_plants",
|
|
483
|
+
record_ids: flatten(lines.records.map((line) => line.data["fld_plant"])),
|
|
484
|
+
workflow_execution_id: toString(sheet.data["fld_run"]),
|
|
321
485
|
});
|
|
486
|
+
reverted += undone.reverted;
|
|
487
|
+
left += size(undone.kept);
|
|
322
488
|
}
|
|
489
|
+
more = size(lines.records) == 500;
|
|
490
|
+
offset += 500;
|
|
323
491
|
}
|
|
492
|
+
|
|
493
|
+
return({ status: "success", message: `${reverted} put back, ${left} left as they stand` });
|
|
324
494
|
```
|
|
325
495
|
|
|
326
496
|
## Table checks
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lotics/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.307.0",
|
|
4
4
|
"description": "Lotics SDK and CLI for AI agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"generate:model-reference": "node --import tsx dev/model_reference_gen.ts",
|
|
23
23
|
"generate:examples": "node --import tsx dev/examples_gen.ts",
|
|
24
24
|
"generate:snapshots": "node --import tsx dev/factory_snapshots.ts",
|
|
25
|
+
"generate:agent-surface": "node --import tsx dev/agent_surface_gen.ts",
|
|
25
26
|
"typecheck": "tsgo --noEmit",
|
|
26
27
|
"lint": "oxlint",
|
|
27
28
|
"test": "vitest run",
|