@lotics/cli 0.198.0 → 0.204.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
@@ -169,7 +169,7 @@ lotics org use acme # switch active org by name (or org id)
169
169
  lotics org use "Acme Corp" # names are case-insensitive
170
170
  ```
171
171
 
172
- The org **id** is the identity; the name is a label cached when the key was saved. Rename an org in the app and the cached label goes stale — the old name still works, the new one matches nothing — until `lotics auth whoami` refreshes it. A name matching two saved orgs is refused rather than guessed; use the id.
172
+ The org **id** is the identity; the name is the label the key was saved under, and it is yours — renaming the org in the app never moves it, so the scripts scoping by it keep running. `lotics auth whoami` learns the new name and records it beside the label; both resolve, and `lotics org` prints `label (server: name)` while they differ. A name matching two saved orgs is refused rather than guessed; use the id.
173
173
 
174
174
  ### Working in parallel (worktrees)
175
175
 
@@ -195,7 +195,7 @@ Every command that resolves a workspace names its target before it acts — `lot
195
195
 
196
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.
197
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.
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. An org keeps the name it was saved under: a server-side rename never moves it, the new name resolves as well, and `lotics org` prints both when they differ.
199
199
 
200
200
  ### Diagnostics
201
201
 
@@ -98,6 +98,8 @@ var __loticsProbe = (() => {
98
98
  }
99
99
  function reportedValues() {
100
100
  const numeric = /^[-+−]?\d[\d.,]*\s?(?:%|[A-Za-z₫đ$€]{1,4})?$/;
101
+ const counted = /(?:%|[A-Za-z₫đ$€]{1,4})$/;
102
+ const delta = /^[↑↓→▲▼]\s*[-+−]?\d[\d.,]*\s?%$/;
101
103
  const dateLike = /\d[/:-]\d/;
102
104
  const skipTag = /^(SCRIPT|STYLE|TITLE|META|LINK|NOSCRIPT|TEMPLATE)$/;
103
105
  const notAValue = 'a, button, [role="tab"], [role="button"], input, th, [role="columnheader"], nav';
@@ -105,6 +107,7 @@ var __loticsProbe = (() => {
105
107
  const nameReach = 360;
106
108
  const values = [];
107
109
  const labels = [];
110
+ let deltas = 0;
108
111
  const walk = (node) => {
109
112
  if (node.nodeType === Node.TEXT_NODE) {
110
113
  const text = (node.textContent || "").trim();
@@ -113,6 +116,10 @@ var __loticsProbe = (() => {
113
116
  if (typeof parent.checkVisibility === "function" && !parent.checkVisibility()) return;
114
117
  const rect = parent.getBoundingClientRect();
115
118
  if (rect.width === 0 && rect.height === 0) return;
119
+ if (delta.test(text)) {
120
+ deltas += 1;
121
+ return;
122
+ }
116
123
  const leaf = { text, left: Math.round(rect.left), right: Math.round(rect.right), top: Math.round(rect.top) };
117
124
  if (!numeric.test(text) || dateLike.test(text)) {
118
125
  labels.push(leaf);
@@ -138,6 +145,7 @@ var __loticsProbe = (() => {
138
145
  }
139
146
  const columns = [];
140
147
  let bare = 0;
148
+ let quantities = 0;
141
149
  for (const cluster of clusters) {
142
150
  const items = [...cluster.items].sort((a, b) => a.top - b.top);
143
151
  const ordinal = items.length >= 3 && items.every((item, index) => item.text === String(index + 1));
@@ -145,10 +153,13 @@ var __loticsProbe = (() => {
145
153
  const top = items[0];
146
154
  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
155
  const beside = labels.filter((label) => Math.abs(label.top - top.top) <= 8 && label.right <= top.left).sort((a, b) => b.right - a.right)[0];
156
+ const scaled = items.filter((item) => counted.test(item.text)).length;
148
157
  bare += items.length;
158
+ quantities += scaled;
149
159
  columns.push({
150
160
  slot: (above ?? beside)?.text.slice(0, 40) ?? "",
151
161
  size: items.length,
162
+ quantities: scaled,
152
163
  samples: items.slice(0, 4).map((item) => item.text)
153
164
  });
154
165
  }
@@ -157,13 +168,37 @@ var __loticsProbe = (() => {
157
168
  const r = el.getBoundingClientRect();
158
169
  return r.width > 0 && r.height > 0;
159
170
  });
171
+ const reads = (el) => {
172
+ const role = el.getAttribute("role");
173
+ if (role !== "meter" && role !== "progressbar") return true;
174
+ const stated = (name) => {
175
+ const raw = el.getAttribute(name);
176
+ return raw === null || raw.trim() === "" ? Number.NaN : Number(raw);
177
+ };
178
+ const bound = (name, fallback) => {
179
+ const value = stated(name);
180
+ return Number.isFinite(value) ? value : fallback;
181
+ };
182
+ const now = stated("aria-valuenow");
183
+ if (!Number.isFinite(now)) return false;
184
+ const floor = bound("aria-valuemin", 0);
185
+ const top = bound("aria-valuemax", 100);
186
+ return top > floor && now < top;
187
+ };
160
188
  const pictures = Array.from(document.querySelectorAll('[role="img"], svg, img')).filter((el) => {
161
189
  if (el.getAttribute("aria-hidden") === "true") return false;
162
190
  const r = el.getBoundingClientRect();
163
191
  return r.width >= pictureMin && r.height >= pictureMin;
164
192
  });
165
- const encoded = drawn.length + pictures.length;
166
- return { total: bare + encoded, bare, encoded, columns };
193
+ const encoded = drawn.filter(reads).length + pictures.length + deltas;
194
+ return {
195
+ total: bare + encoded,
196
+ bare,
197
+ quantities,
198
+ encoded,
199
+ devices: drawn.length + pictures.length + deltas,
200
+ columns
201
+ };
167
202
  }
168
203
  function internalIds() {
169
204
  const skipTag = /^(SCRIPT|STYLE|TITLE|META|LINK|NOSCRIPT|TEMPLATE)$/;
@@ -199,6 +234,8 @@ var __loticsProbe = (() => {
199
234
  }
200
235
  function truncatedLeaves() {
201
236
  const skipTag = /^(SCRIPT|STYLE|TITLE|META|LINK|NOSCRIPT|TEMPLATE)$/;
237
+ const clippedAway = /^inset\(\s*50%/;
238
+ const breakable = /[\t\n\v\f\r \u200b\u3000]/;
202
239
  const cut = [];
203
240
  let total = 0;
204
241
  for (const el of Array.from(document.querySelectorAll("*"))) {
@@ -206,8 +243,17 @@ var __loticsProbe = (() => {
206
243
  const text = (el.textContent || "").trim();
207
244
  if (!text) continue;
208
245
  if (typeof el.checkVisibility === "function" && !el.checkVisibility()) continue;
246
+ const style = getComputedStyle(el);
247
+ const box = el.getBoundingClientRect();
248
+ if (box.width <= 1 && box.height <= 1) continue;
249
+ if (clippedAway.test(style.getPropertyValue("clip-path"))) continue;
209
250
  total += 1;
210
- if (getComputedStyle(el).overflow === "visible") continue;
251
+ if (style.overflow === "visible") continue;
252
+ const states = (property) => {
253
+ const value = style.getPropertyValue(property);
254
+ return value !== "" && value !== "none";
255
+ };
256
+ if ((states("-webkit-line-clamp") || states("line-clamp")) && breakable.test(text)) continue;
211
257
  if (el.scrollWidth > el.clientWidth + 1) {
212
258
  cut.push({ text: text.slice(0, 40), axis: "width", has: el.clientWidth, needs: el.scrollWidth });
213
259
  } else if (el.scrollHeight > el.clientHeight + 1) {
@@ -266,6 +312,7 @@ var __loticsProbe = (() => {
266
312
  if (depth === 0 || el.children.length === 0) return null;
267
313
  return leafOf(el.children[0], depth - 1);
268
314
  };
315
+ const sameColumn = 8;
269
316
  let total = 0;
270
317
  const overDemoted = [];
271
318
  for (const el of Array.from(document.querySelectorAll("div"))) {
@@ -279,6 +326,7 @@ var __loticsProbe = (() => {
279
326
  const a = first.getBoundingClientRect();
280
327
  const b = second.getBoundingClientRect();
281
328
  if (b.top < a.bottom - 2) continue;
329
+ if (Math.abs(b.left - a.left) > sameColumn) continue;
282
330
  if (a.height === 0 || b.height === 0) continue;
283
331
  total += 1;
284
332
  if (px(supporting) < px(subject) && weight(subject) > weight(supporting)) {
@@ -350,7 +398,7 @@ var __loticsProbe = (() => {
350
398
  },
351
399
  reported_values: {
352
400
  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."
401
+ 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; a meter whose maximum is the bound it alarms at reads the same on every row, so it encodes nothing."
354
402
  },
355
403
  internal_ids: {
356
404
  section: "8",
@@ -362,7 +410,7 @@ var __loticsProbe = (() => {
362
410
  },
363
411
  truncation: {
364
412
  section: "8g",
365
- fix: "Shed a trailing column or let the subject take two lines, so the distinguishing half of the name survives."
413
+ fix: "Shed a trailing column, or widen this one until the value fits \u2014 a date, a code or a figure cut inside itself reads as a different one."
366
414
  },
367
415
  banned_glyph: {
368
416
  section: "10",
@@ -395,6 +443,7 @@ var __loticsProbe = (() => {
395
443
  var MONEY_EDGES_ALLOWED = 2;
396
444
  var MONEY_COLUMN_MIN = 3;
397
445
  var BARE_VALUES_FLOOR = 8;
446
+ var QUANTITY_COLUMN_MIN = 2;
398
447
  function run(options) {
399
448
  const findings = [];
400
449
  const money = moneyColumns();
@@ -409,14 +458,15 @@ var __loticsProbe = (() => {
409
458
  });
410
459
  }
411
460
  const values = reportedValues();
412
- if (values.bare >= BARE_VALUES_FLOOR && values.encoded === 0) {
461
+ const measurable = values.columns.some((column) => column.quantities >= QUANTITY_COLUMN_MIN) || values.devices > 0;
462
+ if (values.bare >= BARE_VALUES_FLOOR && values.encoded === 0 && measurable) {
413
463
  findings.push({
414
464
  probe: "reported_values",
415
465
  ...signature("reported_values"),
416
466
  total: values.total,
417
467
  count: values.bare,
418
468
  detail: [
419
- `${values.bare} values as bare text, 0 as a mark, meter, chart or picture`,
469
+ values.devices === 0 ? `${values.bare} values as bare text, nothing drawn as a mark, meter, chart or picture` : `${values.bare} values as bare text; ${values.devices} drawn, none of them reading a value`,
420
470
  ...values.columns.slice(0, 5).map((column) => ` ${column.slot === "" ? "(unnamed)" : column.slot}: ${column.size} \u2014 ${column.samples.join(", ")}`)
421
471
  ]
422
472
  });
@@ -487,7 +537,9 @@ var __loticsProbe = (() => {
487
537
  leaves: textLeafCount(),
488
538
  money: money.total,
489
539
  bare: values.bare,
540
+ quantities: values.quantities,
490
541
  encoded: values.encoded,
542
+ devices: values.devices,
491
543
  strips: strips.counted
492
544
  }
493
545
  };