@lotics/cli 0.197.0 → 0.198.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/README.md CHANGED
@@ -92,6 +92,12 @@ It creates no apps of its own: build one in the workspace afterwards
92
92
  copied in — bound onto the tables the model just made — as part of the same run.
93
93
  `setup` refuses a table name your workspace already has; `lotics scaffold apply model.json` is the
94
94
  additive verb — it adopts that table, adds what the model declares beyond it, and deletes nothing.
95
+ Because it never renames, the file and the workspace drift: `lotics scaffold diff model.json`
96
+ prints exactly where and exits non-zero on any difference, and
97
+ `lotics field rename <table> <field> "<new label>"` moves a label on the platform, in the file
98
+ (`--model`) and in every app bound to it (`--apps`) at once. `lotics scaffold apply model.json
99
+ --documents` writes only the file cells of the model's rows, onto the records the first run made —
100
+ rows land only into empty tables, so it is the one way back for attachments that were missed.
95
101
 
96
102
  The other direction, once a workspace works: `lotics scaffold export > model.json` prints its
97
103
  tables (or only `--tables tbl_a,tbl_b`) as that same file, findings on stderr. It is a starting
@@ -177,7 +183,7 @@ lotics workspace select wks_... # records the workspace in the local pin
177
183
 
178
184
  Each worktree resolves independently; switching the global default in another shell leaves pinned worktrees untouched. `.lotics/` should be gitignored.
179
185
 
180
- Every command that resolves a workspace names its target before it acts — `lotics → <org> / <workspace>` on **stderr**, so stdout stays clean for piping. When nothing in the directory or environment chose the org and it came from the machine-wide default, the line says so and prints the `--local` command to pin — that default is the one another shell can move between two of your commands. `lotics auth whoami` reports the same resolution on demand, including which source won.
186
+ Every command that resolves a workspace names its target before it acts — `lotics → Starters / Equipment & Projects` on **stderr**, so stdout stays clean for piping. Both halves are NAMES, since the line exists to be read before a destructive write; the workspace's name is cached beside its id the first time any command lists it, and until then the line prints the id and says the name is not known yet. When nothing in the directory or environment chose the org and it came from the machine-wide default, the line says so and prints the `--local` command to pin — that default is the one another shell can move between two of your commands. `lotics auth whoami` reports the same resolution on demand, including which source won.
181
187
 
182
188
  ### Resolution precedence
183
189
 
@@ -189,6 +195,8 @@ Every command that resolves a workspace names its target before it acts — `lot
189
195
 
190
196
  `LOTICS_WORKSPACE` (or `--workspace <id>` / `-w`) overrides the workspace at any level. For ephemeral or CI use, set `LOTICS_API_KEY` instead of saving anything.
191
197
 
198
+ **`LOTICS_ORG` is resolved once, before any command runs.** A value matching no saved credential refuses every verb with one sentence — a read, a write, and a check that needs no credential alike — and the refusal lists the orgs this machine does hold, so it is answerable without another command. It refuses even when a key arrives another way: a variable that scopes the command must not go unread while `LOTICS_API_KEY` sends the write somewhere else. When the variable resolves AND a key is supplied, the key decides the org and the command says so. A server-side rename leaves the cached org name stale, so name an org by its `org_` id when a concurrent session may rename it.
199
+
192
200
  ### Diagnostics
193
201
 
194
202
  Every request identifies the CLI (`user-agent: lotics-cli/<version> node/<v> <platform>`) and names the command that made it (`x-lotics-cli-command: app.workflow.set`), so a failure in the server's logs can be traced to the verb and version that produced it. Neither header carries arguments: the command chain stops before any id, path, `@file`, or JSON payload.
@@ -353,6 +361,13 @@ lotics app codegen # import { F, OPT } from "../.lotics/
353
361
  # the code calls that nothing bound, undeclared capabilities, query drift. Exits 1 on what a deploy refuses, so CI can gate on it.
354
362
  lotics app check
355
363
 
364
+ # --screens adds the rendered surface: each screen the app's navigation reaches,
365
+ # and the first record each one opens onto a PAGE, headless in Chrome at 1280
366
+ # and 375, measured against @lotics/ui docs/reviewing.md. --screen and --width
367
+ # narrow it while you iterate on one screen.
368
+ lotics app check --screens
369
+ lotics app check --screens --screen "Đơn hàng" --width 375
370
+
356
371
  # Running an app's alias is a TOOL call, like every other tool. Exits non-zero
357
372
  # when the RUN failed, not only when the call did.
358
373
  lotics run run_app_workflow '{"app_id":"app_...","alias":"issueInvoice","inputs":{"record_id":"rec_..."}}'
@@ -365,7 +380,8 @@ lotics run run_app_workflow '{...}' --cleanup # also deletes created records (
365
380
  # Edit workflow bodies as files. `app pull` writes src/workflows/<alias>.ts (the
366
381
  # faithful server source, wrapped + referencing its .lotics/workflows/<alias>.globals.d.ts);
367
382
  # edit the body, then push it back through set_app_workflow — the server verifies it
368
- # (deploy still never authors workflows). `app workflow check` runs the server's own
383
+ # (a deploy calls the same verb for every alias the project holds ahead of the app, so
384
+ # this is the one-alias spelling of it). `app workflow check` runs the server's own
369
385
  # JS-subset parser + type-check locally, so a body that passes is one `set` will accept
370
386
  # (`set` still adds name resolution + lint + structural checks, which need the workspace):
371
387
  lotics app workflow pull # rewrite src/workflows/*.ts + globals from the server
@@ -394,7 +410,7 @@ LOTICS_UI_SRC=/abs/monorepo/packages/ui/src lotics app deploy -m "..." # warns
394
410
  lotics app dev # unset ⇒ @lotics/ui resolves from node_modules again
395
411
  ```
396
412
 
397
- `app codegen` reads `package.json#lotics.queries` to decide which tables to put in `app_fields.ts`; widen the set with `package.json#lotics.codegen.tables` (an array of `tbl_…` ids) for tables the app only writes via workflows.
413
+ `app codegen` puts a table in `app_fields.ts` when a declared query reads it or a bound workflow writes it — the queries come from `package.json#lotics.queries`, the written tables from each binding's own `table_ids`, so a table only a workflow body touches is addressable by alias with nothing to declare.
398
414
 
399
415
  ## SDK
400
416
 
@@ -21,9 +21,12 @@ var __loticsProbe = (() => {
21
21
  // src/probe_page.ts
22
22
  var probe_page_exports = {};
23
23
  __export(probe_page_exports, {
24
- pressTab: () => pressTab,
24
+ PROBES: () => PROBES,
25
+ openRecord: () => openRecord,
26
+ openScreen: () => openScreen,
27
+ recordRows: () => recordRows,
25
28
  run: () => run,
26
- tabs: () => tabs,
29
+ screens: () => screens,
27
30
  textLeafCount: () => textLeafCount
28
31
  });
29
32
 
@@ -99,23 +102,57 @@ var __loticsProbe = (() => {
99
102
  const skipTag = /^(SCRIPT|STYLE|TITLE|META|LINK|NOSCRIPT|TEMPLATE)$/;
100
103
  const notAValue = 'a, button, [role="tab"], [role="button"], input, th, [role="columnheader"], nav';
101
104
  const pictureMin = 32;
102
- const samples = [];
103
- let bare = 0;
105
+ const nameReach = 360;
106
+ const values = [];
107
+ const labels = [];
104
108
  const walk = (node) => {
105
109
  if (node.nodeType === Node.TEXT_NODE) {
106
110
  const text = (node.textContent || "").trim();
107
111
  const parent = node.parentElement;
108
112
  if (!text || !parent || skipTag.test(parent.tagName)) return;
109
113
  if (typeof parent.checkVisibility === "function" && !parent.checkVisibility()) return;
110
- if (!numeric.test(text) || dateLike.test(text)) return;
114
+ const rect = parent.getBoundingClientRect();
115
+ if (rect.width === 0 && rect.height === 0) return;
116
+ const leaf = { text, left: Math.round(rect.left), right: Math.round(rect.right), top: Math.round(rect.top) };
117
+ if (!numeric.test(text) || dateLike.test(text)) {
118
+ labels.push(leaf);
119
+ return;
120
+ }
111
121
  if (parent.closest(notAValue)) return;
112
- bare += 1;
113
- if (samples.length < 6) samples.push(text);
122
+ values.push(leaf);
114
123
  return;
115
124
  }
116
125
  node.childNodes.forEach(walk);
117
126
  };
118
127
  walk(document.body);
128
+ const clusters = [];
129
+ for (const leaf of [...values].sort((a, b) => a.left - b.left || a.top - b.top)) {
130
+ const cluster = clusters.find((c) => leaf.left < c.right && leaf.right > c.left);
131
+ if (cluster !== void 0) {
132
+ cluster.items.push(leaf);
133
+ cluster.left = Math.min(cluster.left, leaf.left);
134
+ cluster.right = Math.max(cluster.right, leaf.right);
135
+ } else {
136
+ clusters.push({ left: leaf.left, right: leaf.right, items: [leaf] });
137
+ }
138
+ }
139
+ const columns = [];
140
+ let bare = 0;
141
+ for (const cluster of clusters) {
142
+ const items = [...cluster.items].sort((a, b) => a.top - b.top);
143
+ const ordinal = items.length >= 3 && items.every((item, index) => item.text === String(index + 1));
144
+ if (ordinal) continue;
145
+ const top = items[0];
146
+ const above = labels.filter((label) => label.top < top.top && top.top - label.top <= nameReach && label.left < cluster.right && label.right > cluster.left).sort((a, b) => b.top - a.top)[0];
147
+ const beside = labels.filter((label) => Math.abs(label.top - top.top) <= 8 && label.right <= top.left).sort((a, b) => b.right - a.right)[0];
148
+ bare += items.length;
149
+ columns.push({
150
+ slot: (above ?? beside)?.text.slice(0, 40) ?? "",
151
+ size: items.length,
152
+ samples: items.slice(0, 4).map((item) => item.text)
153
+ });
154
+ }
155
+ columns.sort((a, b) => b.size - a.size);
119
156
  const drawn = Array.from(document.querySelectorAll('[role="meter"], [role="progressbar"], canvas')).filter((el) => {
120
157
  const r = el.getBoundingClientRect();
121
158
  return r.width > 0 && r.height > 0;
@@ -126,7 +163,7 @@ var __loticsProbe = (() => {
126
163
  return r.width >= pictureMin && r.height >= pictureMin;
127
164
  });
128
165
  const encoded = drawn.length + pictures.length;
129
- return { total: bare + encoded, bare, encoded, samples };
166
+ return { total: bare + encoded, bare, encoded, columns };
130
167
  }
131
168
  function internalIds() {
132
169
  const skipTag = /^(SCRIPT|STYLE|TITLE|META|LINK|NOSCRIPT|TEMPLATE)$/;
@@ -149,11 +186,16 @@ var __loticsProbe = (() => {
149
186
  return { total, ids: [...ids] };
150
187
  }
151
188
  function tabStrips() {
152
- const strips = Array.from(document.querySelectorAll('[role="tablist"]')).filter((el) => {
189
+ const all = Array.from(document.querySelectorAll('[role="tablist"]')).filter((el) => {
153
190
  const r = el.getBoundingClientRect();
154
191
  return r.width > 0 && r.height > 0;
155
- }).map((el) => Array.from(el.querySelectorAll('[role="tab"]')).map((tab) => (tab.textContent || "").trim()));
156
- return { total: strips.length, strips };
192
+ });
193
+ const counted = all.filter((el) => el.getAttribute("data-order") !== "sequence");
194
+ return {
195
+ total: all.length,
196
+ counted: counted.length,
197
+ strips: counted.map((el) => Array.from(el.querySelectorAll('[role="tab"]')).map((tab) => (tab.textContent || "").trim()))
198
+ };
157
199
  }
158
200
  function truncatedLeaves() {
159
201
  const skipTag = /^(SCRIPT|STYLE|TITLE|META|LINK|NOSCRIPT|TEMPLATE)$/;
@@ -301,6 +343,55 @@ var __loticsProbe = (() => {
301
343
  }
302
344
 
303
345
  // src/probe_page.ts
346
+ var PROBES = {
347
+ money_edges: {
348
+ section: "6",
349
+ fix: "One right edge per money column: give every figure in it the same alignment and the same unit."
350
+ },
351
+ reported_values: {
352
+ section: "9",
353
+ fix: "Encode the column the screen is ABOUT \u2014 a mark, a meter, a chart or a picture \u2014 and leave the rest as text."
354
+ },
355
+ internal_ids: {
356
+ section: "8",
357
+ fix: "A key is not a name: render the record's own label, and keep the id in the link."
358
+ },
359
+ tab_strips: {
360
+ section: "8a",
361
+ fix: "One navigation strip per screen. A second is a filter wearing navigation's clothes \u2014 draw it as one."
362
+ },
363
+ truncation: {
364
+ section: "8g",
365
+ fix: "Shed a trailing column or let the subject take two lines, so the distinguishing half of the name survives."
366
+ },
367
+ banned_glyph: {
368
+ section: "10",
369
+ fix: "No punctuation doing a word's job \u2014 separate the two facts with layout, or write the word."
370
+ },
371
+ compact_money_words: {
372
+ section: "10",
373
+ fix: "Abbreviate in the currency's own words: tr/t\u1EF7 beside \u20AB, K/M/B beside $ and \u20AC."
374
+ },
375
+ stacked_pairs: {
376
+ section: "1",
377
+ fix: "Demote once, not twice: either the size steps down or the subject gets the weight."
378
+ },
379
+ row_marks: {
380
+ section: "9",
381
+ fix: "One rung per row: a mark's size follows what the row is ABOUT, so the supporting mark takes the smaller one."
382
+ },
383
+ render: {
384
+ section: "8",
385
+ fix: "Open the screen and read it: nothing below this was measured, so a clean run over the rest means nothing."
386
+ },
387
+ settle: {
388
+ section: "9c",
389
+ fix: "A screen that never stops changing has a render that re-renders or a request that never lands \u2014 find which."
390
+ }
391
+ };
392
+ function signature(probe) {
393
+ return PROBES[probe];
394
+ }
304
395
  var MONEY_EDGES_ALLOWED = 2;
305
396
  var MONEY_COLUMN_MIN = 3;
306
397
  var BARE_VALUES_FLOOR = 8;
@@ -311,6 +402,7 @@ var __loticsProbe = (() => {
311
402
  if (ragged.length > 0) {
312
403
  findings.push({
313
404
  probe: "money_edges",
405
+ ...signature("money_edges"),
314
406
  total: money.total,
315
407
  count: ragged.length,
316
408
  detail: ragged.slice(0, 5).map((column) => `x=${column.x}: ${column.edges.length} right edges over ${column.size} figures \u2014 ${column.samples.join(" | ")}`)
@@ -320,21 +412,26 @@ var __loticsProbe = (() => {
320
412
  if (values.bare >= BARE_VALUES_FLOOR && values.encoded === 0) {
321
413
  findings.push({
322
414
  probe: "reported_values",
415
+ ...signature("reported_values"),
323
416
  total: values.total,
324
417
  count: values.bare,
325
- detail: [`${values.bare} values as bare text, 0 as a mark, meter, chart or picture \u2014 e.g. ${values.samples.join(", ")}`]
418
+ detail: [
419
+ `${values.bare} values as bare text, 0 as a mark, meter, chart or picture`,
420
+ ...values.columns.slice(0, 5).map((column) => ` ${column.slot === "" ? "(unnamed)" : column.slot}: ${column.size} \u2014 ${column.samples.join(", ")}`)
421
+ ]
326
422
  });
327
423
  }
328
424
  const ids = internalIds();
329
425
  if (ids.ids.length > 0) {
330
- findings.push({ probe: "internal_ids", total: ids.total, count: ids.ids.length, detail: ids.ids.slice(0, 5) });
426
+ findings.push({ probe: "internal_ids", ...signature("internal_ids"), total: ids.total, count: ids.ids.length, detail: ids.ids.slice(0, 5) });
331
427
  }
332
428
  const strips = tabStrips();
333
- if (strips.total > 1) {
429
+ if (strips.counted > 1) {
334
430
  findings.push({
335
431
  probe: "tab_strips",
432
+ ...signature("tab_strips"),
336
433
  total: strips.total,
337
- count: strips.total,
434
+ count: strips.counted,
338
435
  detail: strips.strips.map((strip) => strip.join(" / "))
339
436
  });
340
437
  }
@@ -343,6 +440,7 @@ var __loticsProbe = (() => {
343
440
  if (cut.cut.length > 0) {
344
441
  findings.push({
345
442
  probe: "truncation",
443
+ ...signature("truncation"),
346
444
  total: cut.total,
347
445
  count: cut.cut.length,
348
446
  detail: cut.cut.slice(0, 5).map((leaf) => `"${leaf.text}" needs ${leaf.needs}px of ${leaf.axis} in ${leaf.has}px`)
@@ -351,12 +449,13 @@ var __loticsProbe = (() => {
351
449
  }
352
450
  const glyphs = bannedGlyphs();
353
451
  if (glyphs.found.length > 0) {
354
- findings.push({ probe: "banned_glyph", total: glyphs.total, count: glyphs.found.length, detail: glyphs.found.slice(0, 5) });
452
+ findings.push({ probe: "banned_glyph", ...signature("banned_glyph"), total: glyphs.total, count: glyphs.found.length, detail: glyphs.found.slice(0, 5) });
355
453
  }
356
454
  const compact = compactMoneyWords();
357
455
  if (compact.foreign.length > 0) {
358
456
  findings.push({
359
457
  probe: "compact_money_words",
458
+ ...signature("compact_money_words"),
360
459
  total: compact.total,
361
460
  count: compact.foreign.length,
362
461
  detail: compact.foreign.slice(0, 5).map((text) => `"${text}" abbreviates in the other currency's words`)
@@ -366,6 +465,7 @@ var __loticsProbe = (() => {
366
465
  if (pairs.overDemoted.length > 0) {
367
466
  findings.push({
368
467
  probe: "stacked_pairs",
468
+ ...signature("stacked_pairs"),
369
469
  total: pairs.total,
370
470
  count: pairs.overDemoted.length,
371
471
  detail: pairs.overDemoted.slice(0, 5).map((pair) => `"${pair.subject}" ${pair.sizes[0]}px/${pair.weights[0]} over "${pair.supporting}" ${pair.sizes[1]}px/${pair.weights[1]}`)
@@ -375,6 +475,7 @@ var __loticsProbe = (() => {
375
475
  if (marks.mismatches.length > 0) {
376
476
  findings.push({
377
477
  probe: "row_marks",
478
+ ...signature("row_marks"),
378
479
  total: marks.total,
379
480
  count: marks.mismatches.length,
380
481
  detail: marks.mismatches.slice(0, 5).map((row) => `y=${row.y}: ${row.marks.join(", ")}`)
@@ -387,7 +488,7 @@ var __loticsProbe = (() => {
387
488
  money: money.total,
388
489
  bare: values.bare,
389
490
  encoded: values.encoded,
390
- strips: strips.total
491
+ strips: strips.counted
391
492
  }
392
493
  };
393
494
  }
@@ -405,16 +506,53 @@ var __loticsProbe = (() => {
405
506
  walk(document.body);
406
507
  return count;
407
508
  }
408
- function tabs() {
509
+ function navItems() {
510
+ const nav = document.querySelector("nav");
511
+ if (nav === null) return [];
512
+ return Array.from(nav.querySelectorAll('a[href], [role="link"]')).filter(
513
+ (item) => (item.textContent || "").trim() !== ""
514
+ );
515
+ }
516
+ function tabItems() {
409
517
  const strip = document.querySelector('[role="tablist"]');
410
518
  if (strip === null) return [];
411
- return Array.from(strip.querySelectorAll('[role="tab"]')).map((tab) => (tab.textContent || "").trim());
519
+ return Array.from(strip.querySelectorAll('[role="tab"]'));
412
520
  }
413
- function pressTab(index) {
414
- const strip = document.querySelector('[role="tablist"]');
415
- const tab = strip?.querySelectorAll('[role="tab"]')[index];
416
- if (!(tab instanceof HTMLElement)) return false;
417
- tab.click();
521
+ function screens() {
522
+ const nav = navItems();
523
+ if (nav.length > 0) return { source: "nav", labels: nav.map((item) => (item.textContent || "").trim()) };
524
+ const tabs = tabItems();
525
+ if (tabs.length > 0) return { source: "tabs", labels: tabs.map((item) => (item.textContent || "").trim()) };
526
+ return { source: "root", labels: [] };
527
+ }
528
+ function openScreen(source, index) {
529
+ const items = source === "nav" ? navItems() : source === "tabs" ? tabItems() : [];
530
+ const item = items[index];
531
+ if (item === void 0) return false;
532
+ item.click();
533
+ return true;
534
+ }
535
+ function recordLinks() {
536
+ const here = location.pathname + location.search;
537
+ const doors = Array.from(document.querySelectorAll('a[href], [data-opens="page"]'));
538
+ return doors.filter((door) => {
539
+ if (door.closest('nav, [role="banner"], [role="tablist"]') !== null) return false;
540
+ if (doors.some((other) => other !== door && other.contains(door))) return false;
541
+ const rect = door.getBoundingClientRect();
542
+ if (rect.width === 0 || rect.height === 0) return false;
543
+ const href = door.getAttribute("href");
544
+ if (href === null) return true;
545
+ if (href === "" || href.startsWith("http") || href.startsWith("#")) return false;
546
+ return href !== here;
547
+ });
548
+ }
549
+ function recordRows() {
550
+ return recordLinks().map((door) => (door.textContent || "").trim() || door.getAttribute("href") || "");
551
+ }
552
+ function openRecord(index) {
553
+ const door = recordLinks()[index];
554
+ if (door === void 0) return false;
555
+ door.click();
418
556
  return true;
419
557
  }
420
558
  return __toCommonJS(probe_page_exports);