@lotics/cli 0.143.0 → 0.143.2

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.
@@ -4563,7 +4563,7 @@ ${e.toString()}`);
4563
4563
  }
4564
4564
  };
4565
4565
  var type2Number = { "boolean": 3, "string": 2, "number": 1 };
4566
- var Infix = {
4566
+ var Infix2 = {
4567
4567
  compareOp: (value1, infix, value2, isArray1, isArray2) => {
4568
4568
  if (value1 == null) value1 = 0;
4569
4569
  if (value2 == null) value2 = 0;
@@ -4654,7 +4654,7 @@ ${e.toString()}`);
4654
4654
  module2.exports = {
4655
4655
  Prefix,
4656
4656
  Postfix,
4657
- Infix,
4657
+ Infix: Infix2,
4658
4658
  Operators: {
4659
4659
  compareOp: ["<", ">", "=", "<>", "<=", ">="],
4660
4660
  concatOp: ["&"],
@@ -4669,7 +4669,7 @@ ${e.toString()}`);
4669
4669
  "../../node_modules/fast-formula-parser/formulas/functions/math.js"(exports2, module2) {
4670
4670
  var FormulaError = require_error();
4671
4671
  var { FormulaHelpers, Types, Factorials, Criteria } = require_helpers();
4672
- var { Infix } = require_operators();
4672
+ var { Infix: Infix2 } = require_operators();
4673
4673
  var H = FormulaHelpers;
4674
4674
  var f = [];
4675
4675
  var fd2 = [];
@@ -5192,7 +5192,7 @@ ${e.toString()}`);
5192
5192
  if (criteria.match === criteria.value.test(value2)) {
5193
5193
  sum += valueToAdd;
5194
5194
  }
5195
- } else if (Infix.compareOp(value2, criteria.op, criteria.value, Array.isArray(value2), isCriteriaArray)) {
5195
+ } else if (Infix2.compareOp(value2, criteria.op, criteria.value, Array.isArray(value2), isCriteriaArray)) {
5196
5196
  sum += valueToAdd;
5197
5197
  }
5198
5198
  });
@@ -11472,7 +11472,7 @@ ${e.toString()}`);
11472
11472
  "../../node_modules/fast-formula-parser/formulas/functions/statistical.js"(exports2, module2) {
11473
11473
  var FormulaError = require_error();
11474
11474
  var { FormulaHelpers, Types, Criteria, Address } = require_helpers();
11475
- var { Infix } = require_operators();
11475
+ var { Infix: Infix2 } = require_operators();
11476
11476
  var H = FormulaHelpers;
11477
11477
  var { DistributionFunctions } = require_distribution();
11478
11478
  var StatisticalFunctions = {
@@ -11534,7 +11534,7 @@ ${e.toString()}`);
11534
11534
  sum += valueToAdd;
11535
11535
  cnt++;
11536
11536
  }
11537
- } else if (Infix.compareOp(value2, criteria.op, criteria.value, Array.isArray(value2), isCriteriaArray)) {
11537
+ } else if (Infix2.compareOp(value2, criteria.op, criteria.value, Array.isArray(value2), isCriteriaArray)) {
11538
11538
  sum += valueToAdd;
11539
11539
  cnt++;
11540
11540
  }
@@ -11573,7 +11573,7 @@ ${e.toString()}`);
11573
11573
  if (criteria.op === "wc") {
11574
11574
  if (criteria.match === criteria.value.test(value2))
11575
11575
  cnt++;
11576
- } else if (Infix.compareOp(value2, criteria.op, criteria.value, Array.isArray(value2), isCriteriaArray)) {
11576
+ } else if (Infix2.compareOp(value2, criteria.op, criteria.value, Array.isArray(value2), isCriteriaArray)) {
11577
11577
  cnt++;
11578
11578
  }
11579
11579
  });
@@ -21126,7 +21126,7 @@ ${e.toString()}`);
21126
21126
  "../../node_modules/fast-formula-parser/grammar/utils.js"(exports2, module2) {
21127
21127
  var FormulaError = require_error();
21128
21128
  var { Address } = require_helpers();
21129
- var { Prefix, Postfix, Infix, Operators } = require_operators();
21129
+ var { Prefix, Postfix, Infix: Infix2, Operators } = require_operators();
21130
21130
  var Collection = require_collection();
21131
21131
  var MAX_ROW = 1048576;
21132
21132
  var MAX_COLUMN = 16384;
@@ -21250,11 +21250,11 @@ ${e.toString()}`);
21250
21250
  if (this.isFormulaError(val2))
21251
21251
  return val2;
21252
21252
  if (Operators.compareOp.includes(infix))
21253
- return Infix.compareOp(val1, infix, val2, isArray1, isArray2);
21253
+ return Infix2.compareOp(val1, infix, val2, isArray1, isArray2);
21254
21254
  else if (Operators.concatOp.includes(infix))
21255
- return Infix.concatOp(val1, infix, val2, isArray1, isArray2);
21255
+ return Infix2.concatOp(val1, infix, val2, isArray1, isArray2);
21256
21256
  else if (Operators.mathOp.includes(infix))
21257
- return Infix.mathOp(val1, infix, val2, isArray1, isArray2);
21257
+ return Infix2.mathOp(val1, infix, val2, isArray1, isArray2);
21258
21258
  else
21259
21259
  throw new Error(`Unrecognized infix: ${infix}`);
21260
21260
  }
@@ -21806,7 +21806,7 @@ ${e.toString()}`);
21806
21806
  "../../node_modules/fast-formula-parser/grammar/dependency/utils.js"(exports2, module2) {
21807
21807
  var FormulaError = require_error();
21808
21808
  var { FormulaHelpers, Types, Address } = require_helpers();
21809
- var { Prefix, Postfix, Infix, Operators } = require_operators();
21809
+ var { Prefix, Postfix, Infix: Infix2, Operators } = require_operators();
21810
21810
  var Collection = require_collection();
21811
21811
  var MAX_ROW = 1048576;
21812
21812
  var MAX_COLUMN = 16384;
@@ -53990,14 +53990,14 @@ ${e.toString()}`);
53990
53990
  if (!range) return void 0;
53991
53991
  const header = [];
53992
53992
  for (let col = range.startCol; col <= range.endCol; col++) {
53993
- const cell = sheet.getCell(rowColToRef(range.startRow, col));
53993
+ const cell = sheet.getCellAt(range.startRow, col);
53994
53994
  header.push(formatHeader(cell?.value));
53995
53995
  }
53996
53996
  const records = [];
53997
53997
  for (let row = range.startRow + 1; row <= range.endRow; row++) {
53998
53998
  const rec = [];
53999
53999
  for (let col = range.startCol; col <= range.endCol; col++) {
54000
- const cell = sheet.getCell(rowColToRef(row, col));
54000
+ const cell = sheet.getCellAt(row, col);
54001
54001
  rec.push(coerceValue(cell?.value));
54002
54002
  }
54003
54003
  records.push(rec);
@@ -54041,23 +54041,70 @@ ${e.toString()}`);
54041
54041
  get size() {
54042
54042
  return this.store.size;
54043
54043
  }
54044
+ /**
54045
+ * The populated extent of this map, memoized until a mutation invalidates it.
54046
+ *
54047
+ * It lives HERE rather than on `SheetModel` because it is a pure function of
54048
+ * the cell set: a cache held next to the data cannot outlive it, so replacing
54049
+ * a sheet's map can never serve the previous map's extent. Every mutator
54050
+ * below drops it, including the packed pair — those bypass `set`/`delete`
54051
+ * entirely and are what the row/column commands use.
54052
+ *
54053
+ * Walking PACKED keys is the other half. `keys()` unpacks each key into an A1
54054
+ * string, which the caller then reparsed with a regex, so the old form packed,
54055
+ * unpacked and reparsed the whole sheet on every call — and it is called once
54056
+ * per oversized range during a recompute.
54057
+ */
54058
+ usedRange() {
54059
+ if (!this.usedRangeCache) {
54060
+ let minRow2 = Infinity;
54061
+ let maxRow2 = 0;
54062
+ let minCol2 = Infinity;
54063
+ let maxCol2 = 0;
54064
+ for (const packed of this.store.keys()) {
54065
+ const { row, col } = unpackRowCol(packed);
54066
+ if (row < minRow2) minRow2 = row;
54067
+ if (row > maxRow2) maxRow2 = row;
54068
+ if (col < minCol2) minCol2 = col;
54069
+ if (col > maxCol2) maxCol2 = col;
54070
+ }
54071
+ this.usedRangeCache = maxRow2 === 0 ? { minRow: 1, maxRow: 1, minCol: 1, maxCol: 1 } : { minRow: minRow2, maxRow: maxRow2, minCol: minCol2, maxCol: maxCol2 };
54072
+ }
54073
+ const { minRow, maxRow, minCol, maxCol } = this.usedRangeCache;
54074
+ return { minRow, maxRow, minCol, maxCol };
54075
+ }
54044
54076
  get(ref2) {
54045
54077
  return this.store.get(packRef(ref2));
54046
54078
  }
54079
+ /**
54080
+ * Read by coordinate, skipping the A1 round trip.
54081
+ *
54082
+ * `get(ref)` costs a number→string→regex→number conversion per call, which
54083
+ * is invisible until a caller reads a range cell by cell: on one real
54084
+ * workbook `refToRowCol` alone was 39% of a full recompute. A caller already
54085
+ * holding (row, col) — the range resolver, any grid walk — should use this.
54086
+ */
54087
+ getAt(row, col) {
54088
+ return this.store.get(packRowCol(row, col));
54089
+ }
54047
54090
  set(ref2, cell) {
54048
54091
  const key = packRef(ref2);
54049
54092
  if (key < 0) return this;
54050
54093
  this.store.set(key, cell);
54094
+ this.usedRangeCache = void 0;
54051
54095
  return this;
54052
54096
  }
54053
54097
  has(ref2) {
54054
54098
  return this.store.has(packRef(ref2));
54055
54099
  }
54056
54100
  delete(ref2) {
54057
- return this.store.delete(packRef(ref2));
54101
+ const deleted = this.store.delete(packRef(ref2));
54102
+ if (deleted) this.usedRangeCache = void 0;
54103
+ return deleted;
54058
54104
  }
54059
54105
  clear() {
54060
54106
  this.store.clear();
54107
+ this.usedRangeCache = void 0;
54061
54108
  }
54062
54109
  *keys() {
54063
54110
  for (const k of this.store.keys()) yield unpackRef(k);
@@ -54085,10 +54132,13 @@ ${e.toString()}`);
54085
54132
  /** Direct packed-key set, for the same hot loops. */
54086
54133
  setPacked(packed, cell) {
54087
54134
  this.store.set(packed, cell);
54135
+ this.usedRangeCache = void 0;
54088
54136
  }
54089
54137
  /** Direct packed-key delete. */
54090
54138
  deletePacked(packed) {
54091
- return this.store.delete(packed);
54139
+ const deleted = this.store.delete(packed);
54140
+ if (deleted) this.usedRangeCache = void 0;
54141
+ return deleted;
54092
54142
  }
54093
54143
  };
54094
54144
  var StyleRegistry = class {
@@ -54271,6 +54321,10 @@ ${e.toString()}`);
54271
54321
  getCell(ref2) {
54272
54322
  return this.cells.get(ref2);
54273
54323
  }
54324
+ /** Read by coordinate, skipping the A1 round trip — see `CellMap.getAt`. */
54325
+ getCellAt(row, col) {
54326
+ return this.cells.getAt(row, col);
54327
+ }
54274
54328
  /**
54275
54329
  * Set cell value with optional inline style. Interns style via the
54276
54330
  * StyleRegistry when available. When `style` is supplied and matches the
@@ -54365,22 +54419,12 @@ ${e.toString()}`);
54365
54419
  this.set(ref2, label, style);
54366
54420
  });
54367
54421
  }
54368
- /** Compute the bounding box of all non-empty cells. */
54422
+ /**
54423
+ * The populated extent of the sheet. Derived from — and cached by — the cell
54424
+ * map, so it cannot outlive the data it describes.
54425
+ */
54369
54426
  getUsedRange() {
54370
- let minRow = Infinity;
54371
- let maxRow = 0;
54372
- let minCol = Infinity;
54373
- let maxCol = 0;
54374
- for (const ref2 of this.cells.keys()) {
54375
- const rc = refToRowCol(ref2);
54376
- if (!rc) continue;
54377
- if (rc.row < minRow) minRow = rc.row;
54378
- if (rc.row > maxRow) maxRow = rc.row;
54379
- if (rc.col < minCol) minCol = rc.col;
54380
- if (rc.col > maxCol) maxCol = rc.col;
54381
- }
54382
- if (maxRow === 0) return { minRow: 1, maxRow: 1, minCol: 1, maxCol: 1 };
54383
- return { minRow, maxRow, minCol, maxCol };
54427
+ return this.cells.usedRange();
54384
54428
  }
54385
54429
  };
54386
54430
  var WorkbookModel = class {
@@ -54627,7 +54671,7 @@ ${e.toString()}`);
54627
54671
  let visible = true;
54628
54672
  for (const filterCol of activeFilters) {
54629
54673
  const col = startCol + filterCol.colIndex;
54630
- const cell = sheet.getCell(rowColToRef(r, col));
54674
+ const cell = sheet.getCellAt(r, col);
54631
54675
  const cellText = cell?.displayValue ?? "";
54632
54676
  if (filterCol.filterValues && filterCol.filterValues.length > 0) {
54633
54677
  if (!filterCol.filterValues.includes(cellText)) {
@@ -61857,6 +61901,28 @@ ${e.toString()}`);
61857
61901
  const bang = key.indexOf("!");
61858
61902
  return { sheet: key.slice(0, bang), ref: key.slice(bang + 1) };
61859
61903
  }
61904
+ var COARSE_COL_SUFFIX = ":*";
61905
+ var COARSE_ROW_PREFIX = "*:";
61906
+ function columnKey(sheet, column) {
61907
+ return `${sheet}!${column}${COARSE_COL_SUFFIX}`;
61908
+ }
61909
+ function rowKey(sheet, row) {
61910
+ return `${sheet}!${COARSE_ROW_PREFIX}${row}`;
61911
+ }
61912
+ function isCoarseKey(key) {
61913
+ return key.endsWith(COARSE_COL_SUFFIX) || key.includes(`!${COARSE_ROW_PREFIX}`);
61914
+ }
61915
+ var A1 = /^([A-Z]+)(\d+)$/;
61916
+ function coarseKeysFor(cellKeyStr) {
61917
+ const bang = cellKeyStr.indexOf("!");
61918
+ if (bang < 0) return [];
61919
+ const sheet = cellKeyStr.slice(0, bang);
61920
+ const ref2 = cellKeyStr.slice(bang + 1);
61921
+ const m = A1.exec(ref2);
61922
+ if (!m) return [];
61923
+ return [columnKey(sheet, m[1]), rowKey(sheet, Number(m[2]))];
61924
+ }
61925
+ var EMPTY = /* @__PURE__ */ new Set();
61860
61926
  var DependencyGraph = class {
61861
61927
  constructor() {
61862
61928
  // Forward edges: cell → cells it references (precedents)
@@ -61891,19 +61957,35 @@ ${e.toString()}`);
61891
61957
  }
61892
61958
  /** Get all cells that directly depend on the given cell. */
61893
61959
  getDirectDependents(cell) {
61894
- return Array.from(this.dependents.get(cell) ?? []);
61960
+ return Array.from(this.dependentsOf(cell));
61961
+ }
61962
+ /**
61963
+ * Everything that references this cell — directly, or through the column or
61964
+ * row node that contains it. The single place `dependents` is read, so a
61965
+ * whole-column reference cannot be forgotten by one traversal and honoured by
61966
+ * another.
61967
+ */
61968
+ dependentsOf(cell) {
61969
+ const direct = this.dependents.get(cell);
61970
+ if (isCoarseKey(cell)) return direct ?? EMPTY;
61971
+ const coarse = coarseKeysFor(cell);
61972
+ let widened;
61973
+ for (const key of coarse) {
61974
+ const via = this.dependents.get(key);
61975
+ if (!via || via.size === 0) continue;
61976
+ widened ?? (widened = new Set(direct));
61977
+ for (const d of via) widened.add(d);
61978
+ }
61979
+ return widened ?? direct ?? EMPTY;
61895
61980
  }
61896
61981
  /** Get all cells that need recalculation when the given cells change, in topological order. */
61897
61982
  getRecalcOrder(changed) {
61898
- const changedSet = new Set(changed);
61899
61983
  const affected = /* @__PURE__ */ new Set();
61900
61984
  const queue = [...changed];
61901
61985
  let head = 0;
61902
61986
  while (head < queue.length) {
61903
61987
  const cell = queue[head++];
61904
- const deps = this.dependents.get(cell);
61905
- if (!deps) continue;
61906
- for (const dep of deps) {
61988
+ for (const dep of this.dependentsOf(cell)) {
61907
61989
  if (!affected.has(dep)) {
61908
61990
  affected.add(dep);
61909
61991
  queue.push(dep);
@@ -61912,15 +61994,11 @@ ${e.toString()}`);
61912
61994
  }
61913
61995
  if (affected.size === 0) return [];
61914
61996
  const inDegree = /* @__PURE__ */ new Map();
61997
+ for (const cell of affected) inDegree.set(cell, 0);
61915
61998
  for (const cell of affected) {
61916
- let count = 0;
61917
- const prec = this.precedents.get(cell);
61918
- if (prec) {
61919
- for (const p of prec) {
61920
- if (affected.has(p)) count++;
61921
- }
61999
+ for (const dep of this.dependentsOf(cell)) {
62000
+ if (affected.has(dep)) inDegree.set(dep, (inDegree.get(dep) ?? 0) + 1);
61922
62001
  }
61923
- inDegree.set(cell, count);
61924
62002
  }
61925
62003
  const result = [];
61926
62004
  const ready = [];
@@ -61931,9 +62009,7 @@ ${e.toString()}`);
61931
62009
  while (readyHead < ready.length) {
61932
62010
  const cell = ready[readyHead++];
61933
62011
  result.push(cell);
61934
- const deps = this.dependents.get(cell);
61935
- if (!deps) continue;
61936
- for (const dep of deps) {
62012
+ for (const dep of this.dependentsOf(cell)) {
61937
62013
  if (!affected.has(dep)) continue;
61938
62014
  const newDeg = (inDegree.get(dep) ?? 1) - 1;
61939
62015
  inDegree.set(dep, newDeg);
@@ -61954,12 +62030,13 @@ ${e.toString()}`);
61954
62030
  const stack = new Array();
61955
62031
  const prec = this.precedents.get(cell);
61956
62032
  if (!prec) return null;
62033
+ const originCoarse = new Set(coarseKeysFor(cell));
61957
62034
  for (const p of prec) {
61958
62035
  stack.push({ cell: p, path: [cell, p] });
61959
62036
  }
61960
62037
  while (stack.length > 0) {
61961
62038
  const { cell: current, path } = stack.pop();
61962
- if (current === cell) return path;
62039
+ if (current === cell || originCoarse.has(current)) return path;
61963
62040
  if (visited.has(current)) continue;
61964
62041
  visited.add(current);
61965
62042
  const nextPrec = this.precedents.get(current);
@@ -62072,6 +62149,20 @@ ${e.toString()}`);
62072
62149
  };
62073
62150
  }
62074
62151
 
62152
+ // ../xlsx/src/blank_comparison.ts
62153
+ var import_operators = __toESM(require_operators(), 1);
62154
+ var patched = false;
62155
+ function applyExcelBlankSemantics() {
62156
+ if (patched) return;
62157
+ patched = true;
62158
+ const original = import_operators.Infix.compareOp;
62159
+ import_operators.Infix.compareOp = (value1, infix, value2, isArray1, isArray2) => {
62160
+ if (value1 == null && typeof value2 === "string") value1 = "";
62161
+ if (value2 == null && typeof value1 === "string") value2 = "";
62162
+ return original(value1, infix, value2, isArray1, isArray2);
62163
+ };
62164
+ }
62165
+
62075
62166
  // ../xlsx/src/formula_engine.ts
62076
62167
  var unwrapArg = (a) => a !== null && typeof a === "object" && "value" in a ? a.value : a;
62077
62168
  function adaptFormulajs(fn) {
@@ -62114,9 +62205,24 @@ ${e.toString()}`);
62114
62205
  bridgeCtxCache = withCtx;
62115
62206
  return withCtx;
62116
62207
  }
62208
+ var MAX_EXPANDED_CELLS = 1e4;
62117
62209
  var _FormulaEngine = class _FormulaEngine {
62118
62210
  constructor(workbook) {
62119
62211
  this.graph = new DependencyGraph();
62212
+ /**
62213
+ * Formula cells (by key) whose text the parser could not read AT ALL, found
62214
+ * while building the graph from a loaded file.
62215
+ *
62216
+ * They are held apart from every other failure because the honest response
62217
+ * differs: a formula that evaluates TO an error has been understood, so
62218
+ * writing `#DIV/0!` reports a real answer, while one we cannot parse has not
62219
+ * been — and overwriting the value its author computed would replace a
62220
+ * correct number with a guess. These keep their cached value and are named in
62221
+ * the report instead. The engine's own rule, one level down: degrade what you
62222
+ * cannot draw, refuse what you cannot read.
62223
+ */
62224
+ this.unreadableFormulas = /* @__PURE__ */ new Set();
62225
+ applyExcelBlankSemantics();
62120
62226
  this.workbook = workbook;
62121
62227
  this.parser = new import_fast_formula_parser.default({
62122
62228
  onCell: (ref2) => this.resolveCell(ref2),
@@ -62137,11 +62243,13 @@ ${e.toString()}`);
62137
62243
  /** Build dependency graph from all formula cells in the workbook. */
62138
62244
  buildGraph() {
62139
62245
  this.graph.clear();
62246
+ this.unreadableFormulas.clear();
62140
62247
  for (const sheet of this.workbook.sheets) {
62141
62248
  for (const [ref2, cell] of sheet.cells) {
62142
62249
  if (!cell.formula) continue;
62143
62250
  const key = cellKey({ sheet: sheet.name, ref: ref2 });
62144
- const deps = this.extractDependencies(cell.formula, sheet.name);
62251
+ const { deps, readable } = this.analyzeFormula(cell.formula, sheet.name);
62252
+ if (!readable) this.unreadableFormulas.add(key);
62145
62253
  this.graph.setDependencies(key, deps);
62146
62254
  }
62147
62255
  }
@@ -62197,8 +62305,13 @@ ${e.toString()}`);
62197
62305
  onCellChanged(sheetName, ref2, formula) {
62198
62306
  const key = cellKey({ sheet: sheetName, ref: ref2 });
62199
62307
  if (formula) {
62200
- const deps = this.extractDependencies(formula, sheetName);
62308
+ const { deps, readable } = this.analyzeFormula(formula, sheetName);
62201
62309
  this.graph.setDependencies(key, deps);
62310
+ this.unreadableFormulas.delete(key);
62311
+ if (!readable) {
62312
+ this.setCellError(key, "#ERROR!");
62313
+ return;
62314
+ }
62202
62315
  const cycle = this.graph.detectCycle(key);
62203
62316
  if (cycle) {
62204
62317
  this.setCellError(key, "#CIRC!");
@@ -62207,6 +62320,7 @@ ${e.toString()}`);
62207
62320
  this.evaluateCell(key);
62208
62321
  } else {
62209
62322
  this.graph.removeCell(key);
62323
+ this.unreadableFormulas.delete(key);
62210
62324
  }
62211
62325
  const order = this.graph.getRecalcOrder([key]);
62212
62326
  for (const depKey of order) {
@@ -62223,6 +62337,7 @@ ${e.toString()}`);
62223
62337
  // ===========================================================================
62224
62338
  /** Evaluate a single formula cell and update its value. */
62225
62339
  evaluateCell(key) {
62340
+ if (this.unreadableFormulas.has(key)) return;
62226
62341
  const { sheet: sheetName, ref: ref2 } = parseCellKey(key);
62227
62342
  const sheetIndex = this.workbook.sheets.findIndex((s) => s.name === sheetName);
62228
62343
  if (sheetIndex < 0) return;
@@ -62271,26 +62386,60 @@ ${e.toString()}`);
62271
62386
  // ===========================================================================
62272
62387
  /** Extract cell/range references from a formula string. Returns empty array for unparseable formulas. */
62273
62388
  extractDependencies(formula, currentSheet) {
62274
- const sheet = this.workbook.sheets.find((s) => s.name === currentSheet);
62275
- const expanded = sheet ? expandStructuredRefs(formula, sheet.tables, 1) : formula;
62276
- const deps = this.depParser.parse(expanded, { sheet: currentSheet, row: 1, col: 1 });
62389
+ return this.analyzeFormula(formula, currentSheet).deps;
62390
+ }
62391
+ /**
62392
+ * Dependencies plus whether the parser could read the formula at all.
62393
+ *
62394
+ * `fast-formula-parser` reports a formula it cannot LEX by throwing, the same
62395
+ * channel it uses for `#VALUE!`, so the lexer failure of a single cell used to
62396
+ * escape `buildGraph` and abort the recompute for the entire workbook —
62397
+ * observed on a real workbook whose `VLOOKUP` pointed at another file
62398
+ * (`'[1]DATA'!B:C`, an external-workbook reference the lexer has no rule for).
62399
+ * Anything the grammar does not cover lands here the same way, so this catches
62400
+ * the class rather than that one syntax.
62401
+ */
62402
+ analyzeFormula(formula, currentSheet) {
62403
+ let deps;
62404
+ try {
62405
+ const sheet = this.workbook.sheets.find((s) => s.name === currentSheet);
62406
+ const expanded = sheet ? expandStructuredRefs(formula, sheet.tables, 1) : formula;
62407
+ deps = this.depParser.parse(expanded, { sheet: currentSheet, row: 1, col: 1 });
62408
+ } catch {
62409
+ return { deps: [], readable: false };
62410
+ }
62277
62411
  const refs = [];
62278
62412
  for (const dep of deps) {
62279
62413
  const depSheet = dep.sheet ?? currentSheet;
62280
62414
  if (dep.from && dep.to) {
62281
- const maxCells = 1e4;
62282
- let count = 0;
62283
- for (let r = dep.from.row; r <= dep.to.row && count < maxCells; r++) {
62284
- for (let c = dep.from.col; c <= dep.to.col && count < maxCells; c++) {
62415
+ const rows = dep.to.row - dep.from.row + 1;
62416
+ const cols = dep.to.col - dep.from.col + 1;
62417
+ if (rows * cols > MAX_EXPANDED_CELLS) {
62418
+ if (cols <= rows) {
62419
+ for (let c = dep.from.col; c <= dep.to.col; c++) {
62420
+ refs.push(columnKey(depSheet, colNumToLetters(c)));
62421
+ }
62422
+ } else {
62423
+ for (let r = dep.from.row; r <= dep.to.row; r++) {
62424
+ refs.push(rowKey(depSheet, r));
62425
+ }
62426
+ }
62427
+ continue;
62428
+ }
62429
+ for (let r = dep.from.row; r <= dep.to.row; r++) {
62430
+ for (let c = dep.from.col; c <= dep.to.col; c++) {
62285
62431
  refs.push(cellKey({ sheet: depSheet, ref: rowColToRef(r, c) }));
62286
- count++;
62287
62432
  }
62288
62433
  }
62289
62434
  } else if (dep.row != null && dep.col != null) {
62290
62435
  refs.push(cellKey({ sheet: depSheet, ref: rowColToRef(dep.row, dep.col) }));
62291
62436
  }
62292
62437
  }
62293
- return refs;
62438
+ return { deps: refs, readable: true };
62439
+ }
62440
+ /** Formula cells the parser could not read, by key. Read by the recompute report. */
62441
+ unreadable() {
62442
+ return this.unreadableFormulas;
62294
62443
  }
62295
62444
  // ===========================================================================
62296
62445
  // Cell value resolution callbacks
@@ -62320,8 +62469,7 @@ ${e.toString()}`);
62320
62469
  for (let r = ref2.from.row; r <= lastRow; r++) {
62321
62470
  const row = [];
62322
62471
  for (let c = ref2.from.col; c <= lastCol; c++) {
62323
- const cell = sheet.getCell(rowColToRef(r, c));
62324
- row.push(cell?.value ?? null);
62472
+ row.push(sheet.getCellAt(r, c)?.value ?? null);
62325
62473
  }
62326
62474
  result.push(row);
62327
62475
  }
@@ -62411,15 +62559,19 @@ ${e.toString()}`);
62411
62559
  const engine = new FormulaEngine(workbook);
62412
62560
  engine.buildGraph();
62413
62561
  engine.evaluateAll();
62414
- return collectRecomputeReport(workbook);
62562
+ return collectRecomputeReport(workbook, engine.unreadable());
62415
62563
  }
62416
- function collectRecomputeReport(workbook) {
62564
+ function collectRecomputeReport(workbook, unreadable) {
62417
62565
  let total = 0;
62418
62566
  const unevaluated = [];
62419
62567
  for (const sheet of workbook.sheets) {
62420
62568
  for (const [ref2, cell] of sheet.cells) {
62421
62569
  if (!cell.formula) continue;
62422
62570
  total++;
62571
+ if (unreadable.has(cellKey({ sheet: sheet.name, ref: ref2 }))) {
62572
+ unevaluated.push({ sheet: sheet.name, ref: ref2, formula: cell.formula, error: "#UNREADABLE!" });
62573
+ continue;
62574
+ }
62423
62575
  if (cell.value === null) {
62424
62576
  unevaluated.push({ sheet: sheet.name, ref: ref2, formula: cell.formula, error: cell.error ?? "#ERROR!" });
62425
62577
  }
package/dist/src/cli.js CHANGED
@@ -16769,7 +16769,7 @@ var require_operators = __commonJS({
16769
16769
  }
16770
16770
  };
16771
16771
  var type2Number = { "boolean": 3, "string": 2, "number": 1 };
16772
- var Infix = {
16772
+ var Infix2 = {
16773
16773
  compareOp: (value1, infix, value2, isArray1, isArray22) => {
16774
16774
  if (value1 == null) value1 = 0;
16775
16775
  if (value2 == null) value2 = 0;
@@ -16860,7 +16860,7 @@ var require_operators = __commonJS({
16860
16860
  module2.exports = {
16861
16861
  Prefix,
16862
16862
  Postfix,
16863
- Infix,
16863
+ Infix: Infix2,
16864
16864
  Operators: {
16865
16865
  compareOp: ["<", ">", "=", "<>", "<=", ">="],
16866
16866
  concatOp: ["&"],
@@ -16875,7 +16875,7 @@ var require_math = __commonJS({
16875
16875
  "../../node_modules/fast-formula-parser/formulas/functions/math.js"(exports2, module2) {
16876
16876
  var FormulaError = require_error();
16877
16877
  var { FormulaHelpers, Types, Factorials, Criteria } = require_helpers();
16878
- var { Infix } = require_operators();
16878
+ var { Infix: Infix2 } = require_operators();
16879
16879
  var H = FormulaHelpers;
16880
16880
  var f = [];
16881
16881
  var fd2 = [];
@@ -17398,7 +17398,7 @@ var require_math = __commonJS({
17398
17398
  if (criteria.match === criteria.value.test(value2)) {
17399
17399
  sum2 += valueToAdd;
17400
17400
  }
17401
- } else if (Infix.compareOp(value2, criteria.op, criteria.value, Array.isArray(value2), isCriteriaArray)) {
17401
+ } else if (Infix2.compareOp(value2, criteria.op, criteria.value, Array.isArray(value2), isCriteriaArray)) {
17402
17402
  sum2 += valueToAdd;
17403
17403
  }
17404
17404
  });
@@ -23678,7 +23678,7 @@ var require_statistical = __commonJS({
23678
23678
  "../../node_modules/fast-formula-parser/formulas/functions/statistical.js"(exports2, module2) {
23679
23679
  var FormulaError = require_error();
23680
23680
  var { FormulaHelpers, Types, Criteria, Address } = require_helpers();
23681
- var { Infix } = require_operators();
23681
+ var { Infix: Infix2 } = require_operators();
23682
23682
  var H = FormulaHelpers;
23683
23683
  var { DistributionFunctions } = require_distribution();
23684
23684
  var StatisticalFunctions = {
@@ -23740,7 +23740,7 @@ var require_statistical = __commonJS({
23740
23740
  sum2 += valueToAdd;
23741
23741
  cnt++;
23742
23742
  }
23743
- } else if (Infix.compareOp(value2, criteria.op, criteria.value, Array.isArray(value2), isCriteriaArray)) {
23743
+ } else if (Infix2.compareOp(value2, criteria.op, criteria.value, Array.isArray(value2), isCriteriaArray)) {
23744
23744
  sum2 += valueToAdd;
23745
23745
  cnt++;
23746
23746
  }
@@ -23779,7 +23779,7 @@ var require_statistical = __commonJS({
23779
23779
  if (criteria.op === "wc") {
23780
23780
  if (criteria.match === criteria.value.test(value2))
23781
23781
  cnt++;
23782
- } else if (Infix.compareOp(value2, criteria.op, criteria.value, Array.isArray(value2), isCriteriaArray)) {
23782
+ } else if (Infix2.compareOp(value2, criteria.op, criteria.value, Array.isArray(value2), isCriteriaArray)) {
23783
23783
  cnt++;
23784
23784
  }
23785
23785
  });
@@ -33332,7 +33332,7 @@ var require_utils2 = __commonJS({
33332
33332
  "../../node_modules/fast-formula-parser/grammar/utils.js"(exports2, module2) {
33333
33333
  var FormulaError = require_error();
33334
33334
  var { Address } = require_helpers();
33335
- var { Prefix, Postfix, Infix, Operators } = require_operators();
33335
+ var { Prefix, Postfix, Infix: Infix2, Operators } = require_operators();
33336
33336
  var Collection = require_collection();
33337
33337
  var MAX_ROW2 = 1048576;
33338
33338
  var MAX_COLUMN2 = 16384;
@@ -33456,11 +33456,11 @@ var require_utils2 = __commonJS({
33456
33456
  if (this.isFormulaError(val2))
33457
33457
  return val2;
33458
33458
  if (Operators.compareOp.includes(infix))
33459
- return Infix.compareOp(val1, infix, val2, isArray1, isArray22);
33459
+ return Infix2.compareOp(val1, infix, val2, isArray1, isArray22);
33460
33460
  else if (Operators.concatOp.includes(infix))
33461
- return Infix.concatOp(val1, infix, val2, isArray1, isArray22);
33461
+ return Infix2.concatOp(val1, infix, val2, isArray1, isArray22);
33462
33462
  else if (Operators.mathOp.includes(infix))
33463
- return Infix.mathOp(val1, infix, val2, isArray1, isArray22);
33463
+ return Infix2.mathOp(val1, infix, val2, isArray1, isArray22);
33464
33464
  else
33465
33465
  throw new Error(`Unrecognized infix: ${infix}`);
33466
33466
  }
@@ -34012,7 +34012,7 @@ var require_utils3 = __commonJS({
34012
34012
  "../../node_modules/fast-formula-parser/grammar/dependency/utils.js"(exports2, module2) {
34013
34013
  var FormulaError = require_error();
34014
34014
  var { FormulaHelpers, Types, Address } = require_helpers();
34015
- var { Prefix, Postfix, Infix, Operators } = require_operators();
34015
+ var { Prefix, Postfix, Infix: Infix2, Operators } = require_operators();
34016
34016
  var Collection = require_collection();
34017
34017
  var MAX_ROW2 = 1048576;
34018
34018
  var MAX_COLUMN2 = 16384;
@@ -73018,6 +73018,7 @@ async function appDeploy(client, args) {
73018
73018
  }
73019
73019
  if (liveAfter) {
73020
73020
  warnIfUnbranded(liveAfter);
73021
+ warnIfUndescribed(liveAfter);
73021
73022
  warnIfUnboundAliases(liveAfter, called);
73022
73023
  warnIfInertAgentTools(liveAfter);
73023
73024
  }
@@ -73056,6 +73057,15 @@ function warnIfUnbranded(app) {
73056
73057
  Find an icon: lotics run search_app_icons '{"query":"<word>"}'`
73057
73058
  );
73058
73059
  }
73060
+ function warnIfUndescribed(app) {
73061
+ if (app.description && app.description.trim() !== "") return;
73062
+ console.error(
73063
+ `
73064
+ \u26A0 This app has no description \u2014 the capability catalog its chat agent reads every turn
73065
+ lists the aliases and says nothing about what to do with them.
73066
+ Set it: lotics run update_app '{"app_id":"${app.id}","description":"<what the app is for, and the standing job it does>"}'`
73067
+ );
73068
+ }
73059
73069
  async function readLiveQueries(client, appId) {
73060
73070
  try {
73061
73071
  const app = await client.getApp(appId);
@@ -73163,6 +73173,7 @@ async function appCheck(client, args = {}) {
73163
73173
  warnIfInertAgentTools(app);
73164
73174
  warnIfProjectFootguns(projectDir, sourceText);
73165
73175
  warnIfUnbranded(app);
73176
+ warnIfUndescribed(app);
73166
73177
  if (!nothingPending(pending)) {
73167
73178
  process.exit(1);
73168
73179
  }
@@ -84774,14 +84785,14 @@ function readSourceData(workbook, cache) {
84774
84785
  if (!range2) return void 0;
84775
84786
  const header = [];
84776
84787
  for (let col = range2.startCol; col <= range2.endCol; col++) {
84777
- const cell = sheet.getCell(rowColToRef(range2.startRow, col));
84788
+ const cell = sheet.getCellAt(range2.startRow, col);
84778
84789
  header.push(formatHeader(cell?.value));
84779
84790
  }
84780
84791
  const records = [];
84781
84792
  for (let row = range2.startRow + 1; row <= range2.endRow; row++) {
84782
84793
  const rec = [];
84783
84794
  for (let col = range2.startCol; col <= range2.endCol; col++) {
84784
- const cell = sheet.getCell(rowColToRef(row, col));
84795
+ const cell = sheet.getCellAt(row, col);
84785
84796
  rec.push(coerceValue(cell?.value));
84786
84797
  }
84787
84798
  records.push(rec);
@@ -86325,23 +86336,70 @@ var CellMap = class {
86325
86336
  get size() {
86326
86337
  return this.store.size;
86327
86338
  }
86339
+ /**
86340
+ * The populated extent of this map, memoized until a mutation invalidates it.
86341
+ *
86342
+ * It lives HERE rather than on `SheetModel` because it is a pure function of
86343
+ * the cell set: a cache held next to the data cannot outlive it, so replacing
86344
+ * a sheet's map can never serve the previous map's extent. Every mutator
86345
+ * below drops it, including the packed pair — those bypass `set`/`delete`
86346
+ * entirely and are what the row/column commands use.
86347
+ *
86348
+ * Walking PACKED keys is the other half. `keys()` unpacks each key into an A1
86349
+ * string, which the caller then reparsed with a regex, so the old form packed,
86350
+ * unpacked and reparsed the whole sheet on every call — and it is called once
86351
+ * per oversized range during a recompute.
86352
+ */
86353
+ usedRange() {
86354
+ if (!this.usedRangeCache) {
86355
+ let minRow2 = Infinity;
86356
+ let maxRow2 = 0;
86357
+ let minCol2 = Infinity;
86358
+ let maxCol2 = 0;
86359
+ for (const packed of this.store.keys()) {
86360
+ const { row, col } = unpackRowCol(packed);
86361
+ if (row < minRow2) minRow2 = row;
86362
+ if (row > maxRow2) maxRow2 = row;
86363
+ if (col < minCol2) minCol2 = col;
86364
+ if (col > maxCol2) maxCol2 = col;
86365
+ }
86366
+ this.usedRangeCache = maxRow2 === 0 ? { minRow: 1, maxRow: 1, minCol: 1, maxCol: 1 } : { minRow: minRow2, maxRow: maxRow2, minCol: minCol2, maxCol: maxCol2 };
86367
+ }
86368
+ const { minRow, maxRow, minCol, maxCol } = this.usedRangeCache;
86369
+ return { minRow, maxRow, minCol, maxCol };
86370
+ }
86328
86371
  get(ref2) {
86329
86372
  return this.store.get(packRef(ref2));
86330
86373
  }
86374
+ /**
86375
+ * Read by coordinate, skipping the A1 round trip.
86376
+ *
86377
+ * `get(ref)` costs a number→string→regex→number conversion per call, which
86378
+ * is invisible until a caller reads a range cell by cell: on one real
86379
+ * workbook `refToRowCol` alone was 39% of a full recompute. A caller already
86380
+ * holding (row, col) — the range resolver, any grid walk — should use this.
86381
+ */
86382
+ getAt(row, col) {
86383
+ return this.store.get(packRowCol(row, col));
86384
+ }
86331
86385
  set(ref2, cell) {
86332
86386
  const key = packRef(ref2);
86333
86387
  if (key < 0) return this;
86334
86388
  this.store.set(key, cell);
86389
+ this.usedRangeCache = void 0;
86335
86390
  return this;
86336
86391
  }
86337
86392
  has(ref2) {
86338
86393
  return this.store.has(packRef(ref2));
86339
86394
  }
86340
86395
  delete(ref2) {
86341
- return this.store.delete(packRef(ref2));
86396
+ const deleted = this.store.delete(packRef(ref2));
86397
+ if (deleted) this.usedRangeCache = void 0;
86398
+ return deleted;
86342
86399
  }
86343
86400
  clear() {
86344
86401
  this.store.clear();
86402
+ this.usedRangeCache = void 0;
86345
86403
  }
86346
86404
  *keys() {
86347
86405
  for (const k of this.store.keys()) yield unpackRef(k);
@@ -86369,10 +86427,13 @@ var CellMap = class {
86369
86427
  /** Direct packed-key set, for the same hot loops. */
86370
86428
  setPacked(packed, cell) {
86371
86429
  this.store.set(packed, cell);
86430
+ this.usedRangeCache = void 0;
86372
86431
  }
86373
86432
  /** Direct packed-key delete. */
86374
86433
  deletePacked(packed) {
86375
- return this.store.delete(packed);
86434
+ const deleted = this.store.delete(packed);
86435
+ if (deleted) this.usedRangeCache = void 0;
86436
+ return deleted;
86376
86437
  }
86377
86438
  };
86378
86439
  var StyleRegistry = class {
@@ -86555,6 +86616,10 @@ var SheetModel = class {
86555
86616
  getCell(ref2) {
86556
86617
  return this.cells.get(ref2);
86557
86618
  }
86619
+ /** Read by coordinate, skipping the A1 round trip — see `CellMap.getAt`. */
86620
+ getCellAt(row, col) {
86621
+ return this.cells.getAt(row, col);
86622
+ }
86558
86623
  /**
86559
86624
  * Set cell value with optional inline style. Interns style via the
86560
86625
  * StyleRegistry when available. When `style` is supplied and matches the
@@ -86649,22 +86714,12 @@ var SheetModel = class {
86649
86714
  this.set(ref2, label, style);
86650
86715
  });
86651
86716
  }
86652
- /** Compute the bounding box of all non-empty cells. */
86717
+ /**
86718
+ * The populated extent of the sheet. Derived from — and cached by — the cell
86719
+ * map, so it cannot outlive the data it describes.
86720
+ */
86653
86721
  getUsedRange() {
86654
- let minRow = Infinity;
86655
- let maxRow = 0;
86656
- let minCol = Infinity;
86657
- let maxCol = 0;
86658
- for (const ref2 of this.cells.keys()) {
86659
- const rc = refToRowCol(ref2);
86660
- if (!rc) continue;
86661
- if (rc.row < minRow) minRow = rc.row;
86662
- if (rc.row > maxRow) maxRow = rc.row;
86663
- if (rc.col < minCol) minCol = rc.col;
86664
- if (rc.col > maxCol) maxCol = rc.col;
86665
- }
86666
- if (maxRow === 0) return { minRow: 1, maxRow: 1, minCol: 1, maxCol: 1 };
86667
- return { minRow, maxRow, minCol, maxCol };
86722
+ return this.cells.usedRange();
86668
86723
  }
86669
86724
  };
86670
86725
  var WorkbookModel = class {
@@ -86911,7 +86966,7 @@ function applyAutoFilterHiddenRows(sheet) {
86911
86966
  let visible = true;
86912
86967
  for (const filterCol of activeFilters) {
86913
86968
  const col = startCol + filterCol.colIndex;
86914
- const cell = sheet.getCell(rowColToRef(r, col));
86969
+ const cell = sheet.getCellAt(r, col);
86915
86970
  const cellText = cell?.displayValue ?? "";
86916
86971
  if (filterCol.filterValues && filterCol.filterValues.length > 0) {
86917
86972
  if (!filterCol.filterValues.includes(cellText)) {
@@ -94141,6 +94196,28 @@ function parseCellKey(key) {
94141
94196
  const bang = key.indexOf("!");
94142
94197
  return { sheet: key.slice(0, bang), ref: key.slice(bang + 1) };
94143
94198
  }
94199
+ var COARSE_COL_SUFFIX = ":*";
94200
+ var COARSE_ROW_PREFIX = "*:";
94201
+ function columnKey(sheet, column) {
94202
+ return `${sheet}!${column}${COARSE_COL_SUFFIX}`;
94203
+ }
94204
+ function rowKey(sheet, row) {
94205
+ return `${sheet}!${COARSE_ROW_PREFIX}${row}`;
94206
+ }
94207
+ function isCoarseKey(key) {
94208
+ return key.endsWith(COARSE_COL_SUFFIX) || key.includes(`!${COARSE_ROW_PREFIX}`);
94209
+ }
94210
+ var A1 = /^([A-Z]+)(\d+)$/;
94211
+ function coarseKeysFor(cellKeyStr) {
94212
+ const bang = cellKeyStr.indexOf("!");
94213
+ if (bang < 0) return [];
94214
+ const sheet = cellKeyStr.slice(0, bang);
94215
+ const ref2 = cellKeyStr.slice(bang + 1);
94216
+ const m = A1.exec(ref2);
94217
+ if (!m) return [];
94218
+ return [columnKey(sheet, m[1]), rowKey(sheet, Number(m[2]))];
94219
+ }
94220
+ var EMPTY = /* @__PURE__ */ new Set();
94144
94221
  var DependencyGraph = class {
94145
94222
  constructor() {
94146
94223
  // Forward edges: cell → cells it references (precedents)
@@ -94175,19 +94252,35 @@ var DependencyGraph = class {
94175
94252
  }
94176
94253
  /** Get all cells that directly depend on the given cell. */
94177
94254
  getDirectDependents(cell) {
94178
- return Array.from(this.dependents.get(cell) ?? []);
94255
+ return Array.from(this.dependentsOf(cell));
94256
+ }
94257
+ /**
94258
+ * Everything that references this cell — directly, or through the column or
94259
+ * row node that contains it. The single place `dependents` is read, so a
94260
+ * whole-column reference cannot be forgotten by one traversal and honoured by
94261
+ * another.
94262
+ */
94263
+ dependentsOf(cell) {
94264
+ const direct = this.dependents.get(cell);
94265
+ if (isCoarseKey(cell)) return direct ?? EMPTY;
94266
+ const coarse = coarseKeysFor(cell);
94267
+ let widened;
94268
+ for (const key of coarse) {
94269
+ const via = this.dependents.get(key);
94270
+ if (!via || via.size === 0) continue;
94271
+ widened ??= new Set(direct);
94272
+ for (const d of via) widened.add(d);
94273
+ }
94274
+ return widened ?? direct ?? EMPTY;
94179
94275
  }
94180
94276
  /** Get all cells that need recalculation when the given cells change, in topological order. */
94181
94277
  getRecalcOrder(changed) {
94182
- const changedSet = new Set(changed);
94183
94278
  const affected = /* @__PURE__ */ new Set();
94184
94279
  const queue = [...changed];
94185
94280
  let head = 0;
94186
94281
  while (head < queue.length) {
94187
94282
  const cell = queue[head++];
94188
- const deps = this.dependents.get(cell);
94189
- if (!deps) continue;
94190
- for (const dep of deps) {
94283
+ for (const dep of this.dependentsOf(cell)) {
94191
94284
  if (!affected.has(dep)) {
94192
94285
  affected.add(dep);
94193
94286
  queue.push(dep);
@@ -94196,15 +94289,11 @@ var DependencyGraph = class {
94196
94289
  }
94197
94290
  if (affected.size === 0) return [];
94198
94291
  const inDegree = /* @__PURE__ */ new Map();
94292
+ for (const cell of affected) inDegree.set(cell, 0);
94199
94293
  for (const cell of affected) {
94200
- let count = 0;
94201
- const prec = this.precedents.get(cell);
94202
- if (prec) {
94203
- for (const p of prec) {
94204
- if (affected.has(p)) count++;
94205
- }
94294
+ for (const dep of this.dependentsOf(cell)) {
94295
+ if (affected.has(dep)) inDegree.set(dep, (inDegree.get(dep) ?? 0) + 1);
94206
94296
  }
94207
- inDegree.set(cell, count);
94208
94297
  }
94209
94298
  const result = [];
94210
94299
  const ready = [];
@@ -94215,9 +94304,7 @@ var DependencyGraph = class {
94215
94304
  while (readyHead < ready.length) {
94216
94305
  const cell = ready[readyHead++];
94217
94306
  result.push(cell);
94218
- const deps = this.dependents.get(cell);
94219
- if (!deps) continue;
94220
- for (const dep of deps) {
94307
+ for (const dep of this.dependentsOf(cell)) {
94221
94308
  if (!affected.has(dep)) continue;
94222
94309
  const newDeg = (inDegree.get(dep) ?? 1) - 1;
94223
94310
  inDegree.set(dep, newDeg);
@@ -94238,12 +94325,13 @@ var DependencyGraph = class {
94238
94325
  const stack = new Array();
94239
94326
  const prec = this.precedents.get(cell);
94240
94327
  if (!prec) return null;
94328
+ const originCoarse = new Set(coarseKeysFor(cell));
94241
94329
  for (const p of prec) {
94242
94330
  stack.push({ cell: p, path: [cell, p] });
94243
94331
  }
94244
94332
  while (stack.length > 0) {
94245
94333
  const { cell: current, path: path12 } = stack.pop();
94246
- if (current === cell) return path12;
94334
+ if (current === cell || originCoarse.has(current)) return path12;
94247
94335
  if (visited.has(current)) continue;
94248
94336
  visited.add(current);
94249
94337
  const nextPrec = this.precedents.get(current);
@@ -94356,6 +94444,20 @@ function parseTableRange(ref2) {
94356
94444
  };
94357
94445
  }
94358
94446
 
94447
+ // ../xlsx/src/blank_comparison.ts
94448
+ var import_operators = __toESM(require_operators(), 1);
94449
+ var patched = false;
94450
+ function applyExcelBlankSemantics() {
94451
+ if (patched) return;
94452
+ patched = true;
94453
+ const original = import_operators.Infix.compareOp;
94454
+ import_operators.Infix.compareOp = (value1, infix, value2, isArray1, isArray22) => {
94455
+ if (value1 == null && typeof value2 === "string") value1 = "";
94456
+ if (value2 == null && typeof value1 === "string") value2 = "";
94457
+ return original(value1, infix, value2, isArray1, isArray22);
94458
+ };
94459
+ }
94460
+
94359
94461
  // ../xlsx/src/formula_engine.ts
94360
94462
  var unwrapArg = (a) => a !== null && typeof a === "object" && "value" in a ? a.value : a;
94361
94463
  function adaptFormulajs(fn) {
@@ -94398,9 +94500,24 @@ function formulajsBridgeWithContext() {
94398
94500
  bridgeCtxCache = withCtx;
94399
94501
  return withCtx;
94400
94502
  }
94503
+ var MAX_EXPANDED_CELLS = 1e4;
94401
94504
  var FormulaEngine = class _FormulaEngine {
94402
94505
  constructor(workbook) {
94403
94506
  this.graph = new DependencyGraph();
94507
+ /**
94508
+ * Formula cells (by key) whose text the parser could not read AT ALL, found
94509
+ * while building the graph from a loaded file.
94510
+ *
94511
+ * They are held apart from every other failure because the honest response
94512
+ * differs: a formula that evaluates TO an error has been understood, so
94513
+ * writing `#DIV/0!` reports a real answer, while one we cannot parse has not
94514
+ * been — and overwriting the value its author computed would replace a
94515
+ * correct number with a guess. These keep their cached value and are named in
94516
+ * the report instead. The engine's own rule, one level down: degrade what you
94517
+ * cannot draw, refuse what you cannot read.
94518
+ */
94519
+ this.unreadableFormulas = /* @__PURE__ */ new Set();
94520
+ applyExcelBlankSemantics();
94404
94521
  this.workbook = workbook;
94405
94522
  this.parser = new import_fast_formula_parser.default({
94406
94523
  onCell: (ref2) => this.resolveCell(ref2),
@@ -94421,11 +94538,13 @@ var FormulaEngine = class _FormulaEngine {
94421
94538
  /** Build dependency graph from all formula cells in the workbook. */
94422
94539
  buildGraph() {
94423
94540
  this.graph.clear();
94541
+ this.unreadableFormulas.clear();
94424
94542
  for (const sheet of this.workbook.sheets) {
94425
94543
  for (const [ref2, cell] of sheet.cells) {
94426
94544
  if (!cell.formula) continue;
94427
94545
  const key = cellKey({ sheet: sheet.name, ref: ref2 });
94428
- const deps = this.extractDependencies(cell.formula, sheet.name);
94546
+ const { deps, readable } = this.analyzeFormula(cell.formula, sheet.name);
94547
+ if (!readable) this.unreadableFormulas.add(key);
94429
94548
  this.graph.setDependencies(key, deps);
94430
94549
  }
94431
94550
  }
@@ -94481,8 +94600,13 @@ var FormulaEngine = class _FormulaEngine {
94481
94600
  onCellChanged(sheetName, ref2, formula) {
94482
94601
  const key = cellKey({ sheet: sheetName, ref: ref2 });
94483
94602
  if (formula) {
94484
- const deps = this.extractDependencies(formula, sheetName);
94603
+ const { deps, readable } = this.analyzeFormula(formula, sheetName);
94485
94604
  this.graph.setDependencies(key, deps);
94605
+ this.unreadableFormulas.delete(key);
94606
+ if (!readable) {
94607
+ this.setCellError(key, "#ERROR!");
94608
+ return;
94609
+ }
94486
94610
  const cycle = this.graph.detectCycle(key);
94487
94611
  if (cycle) {
94488
94612
  this.setCellError(key, "#CIRC!");
@@ -94491,6 +94615,7 @@ var FormulaEngine = class _FormulaEngine {
94491
94615
  this.evaluateCell(key);
94492
94616
  } else {
94493
94617
  this.graph.removeCell(key);
94618
+ this.unreadableFormulas.delete(key);
94494
94619
  }
94495
94620
  const order = this.graph.getRecalcOrder([key]);
94496
94621
  for (const depKey of order) {
@@ -94507,6 +94632,7 @@ var FormulaEngine = class _FormulaEngine {
94507
94632
  // ===========================================================================
94508
94633
  /** Evaluate a single formula cell and update its value. */
94509
94634
  evaluateCell(key) {
94635
+ if (this.unreadableFormulas.has(key)) return;
94510
94636
  const { sheet: sheetName, ref: ref2 } = parseCellKey(key);
94511
94637
  const sheetIndex = this.workbook.sheets.findIndex((s) => s.name === sheetName);
94512
94638
  if (sheetIndex < 0) return;
@@ -94555,26 +94681,60 @@ var FormulaEngine = class _FormulaEngine {
94555
94681
  // ===========================================================================
94556
94682
  /** Extract cell/range references from a formula string. Returns empty array for unparseable formulas. */
94557
94683
  extractDependencies(formula, currentSheet) {
94558
- const sheet = this.workbook.sheets.find((s) => s.name === currentSheet);
94559
- const expanded = sheet ? expandStructuredRefs(formula, sheet.tables, 1) : formula;
94560
- const deps = this.depParser.parse(expanded, { sheet: currentSheet, row: 1, col: 1 });
94684
+ return this.analyzeFormula(formula, currentSheet).deps;
94685
+ }
94686
+ /**
94687
+ * Dependencies plus whether the parser could read the formula at all.
94688
+ *
94689
+ * `fast-formula-parser` reports a formula it cannot LEX by throwing, the same
94690
+ * channel it uses for `#VALUE!`, so the lexer failure of a single cell used to
94691
+ * escape `buildGraph` and abort the recompute for the entire workbook —
94692
+ * observed on a real workbook whose `VLOOKUP` pointed at another file
94693
+ * (`'[1]DATA'!B:C`, an external-workbook reference the lexer has no rule for).
94694
+ * Anything the grammar does not cover lands here the same way, so this catches
94695
+ * the class rather than that one syntax.
94696
+ */
94697
+ analyzeFormula(formula, currentSheet) {
94698
+ let deps;
94699
+ try {
94700
+ const sheet = this.workbook.sheets.find((s) => s.name === currentSheet);
94701
+ const expanded = sheet ? expandStructuredRefs(formula, sheet.tables, 1) : formula;
94702
+ deps = this.depParser.parse(expanded, { sheet: currentSheet, row: 1, col: 1 });
94703
+ } catch {
94704
+ return { deps: [], readable: false };
94705
+ }
94561
94706
  const refs = [];
94562
94707
  for (const dep of deps) {
94563
94708
  const depSheet = dep.sheet ?? currentSheet;
94564
94709
  if (dep.from && dep.to) {
94565
- const maxCells = 1e4;
94566
- let count = 0;
94567
- for (let r = dep.from.row; r <= dep.to.row && count < maxCells; r++) {
94568
- for (let c = dep.from.col; c <= dep.to.col && count < maxCells; c++) {
94710
+ const rows = dep.to.row - dep.from.row + 1;
94711
+ const cols = dep.to.col - dep.from.col + 1;
94712
+ if (rows * cols > MAX_EXPANDED_CELLS) {
94713
+ if (cols <= rows) {
94714
+ for (let c = dep.from.col; c <= dep.to.col; c++) {
94715
+ refs.push(columnKey(depSheet, colNumToLetters(c)));
94716
+ }
94717
+ } else {
94718
+ for (let r = dep.from.row; r <= dep.to.row; r++) {
94719
+ refs.push(rowKey(depSheet, r));
94720
+ }
94721
+ }
94722
+ continue;
94723
+ }
94724
+ for (let r = dep.from.row; r <= dep.to.row; r++) {
94725
+ for (let c = dep.from.col; c <= dep.to.col; c++) {
94569
94726
  refs.push(cellKey({ sheet: depSheet, ref: rowColToRef(r, c) }));
94570
- count++;
94571
94727
  }
94572
94728
  }
94573
94729
  } else if (dep.row != null && dep.col != null) {
94574
94730
  refs.push(cellKey({ sheet: depSheet, ref: rowColToRef(dep.row, dep.col) }));
94575
94731
  }
94576
94732
  }
94577
- return refs;
94733
+ return { deps: refs, readable: true };
94734
+ }
94735
+ /** Formula cells the parser could not read, by key. Read by the recompute report. */
94736
+ unreadable() {
94737
+ return this.unreadableFormulas;
94578
94738
  }
94579
94739
  // ===========================================================================
94580
94740
  // Cell value resolution callbacks
@@ -94611,8 +94771,7 @@ var FormulaEngine = class _FormulaEngine {
94611
94771
  for (let r = ref2.from.row; r <= lastRow; r++) {
94612
94772
  const row = [];
94613
94773
  for (let c = ref2.from.col; c <= lastCol; c++) {
94614
- const cell = sheet.getCell(rowColToRef(r, c));
94615
- row.push(cell?.value ?? null);
94774
+ row.push(sheet.getCellAt(r, c)?.value ?? null);
94616
94775
  }
94617
94776
  result.push(row);
94618
94777
  }
@@ -94696,15 +94855,19 @@ function recomputeAll(workbook) {
94696
94855
  const engine = new FormulaEngine(workbook);
94697
94856
  engine.buildGraph();
94698
94857
  engine.evaluateAll();
94699
- return collectRecomputeReport(workbook);
94858
+ return collectRecomputeReport(workbook, engine.unreadable());
94700
94859
  }
94701
- function collectRecomputeReport(workbook) {
94860
+ function collectRecomputeReport(workbook, unreadable) {
94702
94861
  let total = 0;
94703
94862
  const unevaluated = [];
94704
94863
  for (const sheet of workbook.sheets) {
94705
94864
  for (const [ref2, cell] of sheet.cells) {
94706
94865
  if (!cell.formula) continue;
94707
94866
  total++;
94867
+ if (unreadable.has(cellKey({ sheet: sheet.name, ref: ref2 }))) {
94868
+ unevaluated.push({ sheet: sheet.name, ref: ref2, formula: cell.formula, error: "#UNREADABLE!" });
94869
+ continue;
94870
+ }
94708
94871
  if (cell.value === null) {
94709
94872
  unevaluated.push({ sheet: sheet.name, ref: ref2, formula: cell.formula, error: cell.error ?? "#ERROR!" });
94710
94873
  }
@@ -95642,9 +95805,9 @@ function applySheetCells(workbook, sheetIndex, def) {
95642
95805
  }
95643
95806
  }
95644
95807
  if (def.rowHeights) {
95645
- for (const [rowKey, height] of Object.entries(def.rowHeights)) {
95646
- const row = Number(rowKey);
95647
- if (!Number.isInteger(row) || row < 1) fail2(`Invalid row in sheet "${def.name}".rowHeights: ${rowKey}`);
95808
+ for (const [rowKey2, height] of Object.entries(def.rowHeights)) {
95809
+ const row = Number(rowKey2);
95810
+ if (!Number.isInteger(row) || row < 1) fail2(`Invalid row in sheet "${def.name}".rowHeights: ${rowKey2}`);
95648
95811
  sheet.rowHeights.set(row, height);
95649
95812
  }
95650
95813
  }
@@ -224,6 +224,10 @@ export declare class LoticsClient {
224
224
  getApp(app_id: string): Promise<{
225
225
  id: string;
226
226
  name: string;
227
+ /** What the app is for. Heads the capability catalog the member's chat agent
228
+ * reads every turn the app is open, so it is where a standing process is
229
+ * stated. Null when unset. */
230
+ description?: string | null;
227
231
  workspace_id: string;
228
232
  current_version_id: string | null;
229
233
  /** Launcher icon — a Lucide name, or an image ref. Null when unset. */
@@ -109,6 +109,13 @@ writes the schema back into the manifest and refreshes that alias's types in pla
109
109
  The alias's `description` rides along from the manifest. It is the one line an agent reads when
110
110
  choosing between your workflows, so write it rather than leaving the generated placeholder.
111
111
 
112
+ **Every workflow you declare is also the chat agent's write surface.** So the alias's *shape* is an
113
+ agent-facing decision, not only a screen-facing one — take a list where one job covers many
114
+ records; say in an optional input's own `description` what omitting it means, since the agent
115
+ cannot see the default your body applies; make the write survive running twice on the same input;
116
+ and gate anything irreversible with `wait_for_approval` inside the body. `lotics docs ai` and
117
+ `lotics docs workflows` carry each of those.
118
+
112
119
  **Static green is not a run.** `check` and `set` prove parse, types, name resolution and lint —
113
120
  they evaluate nothing. Rehearse before the first live run:
114
121
 
@@ -196,12 +203,19 @@ lotics app deploy -m "<what changed + why>"
196
203
  `-m` is optional; the deploy derives a message from what it pushed. Write one when the *why* is
197
204
  worth keeping.
198
205
 
199
- Then set the icon and colour — the most-forgotten step, and the deploy warns when they are unset:
206
+ Then set the icon, the colour and the app's own `description` — the most-forgotten step, which is
207
+ why `deploy` and `check` both warn while any of them is unset:
200
208
 
201
209
  ```
202
- lotics run update_app '{"app_id":"…","icon":"<lucide-name>","theme":{"color":"blue"}}'
210
+ lotics run update_app '{"app_id":"…","icon":"<lucide-name>","theme":{"color":"blue"},
211
+ "description":"<what the app is for, and the standing job it does>"}'
203
212
  ```
204
213
 
214
+ The `description` is not a label. It heads the capability listing the member's chat agent reads on
215
+ **every** turn, so a standing process the app expects that agent to carry out belongs there and
216
+ nowhere else — not in workspace instructions and not in a knowledge doc, both of which apply to
217
+ every app at once (`lotics docs ai`).
218
+
205
219
  Finally, update the app's own `README.md` on any model or behaviour change and redeploy, so the
206
220
  brief travels with the app rather than living in whoever built it.
207
221
 
@@ -223,7 +237,7 @@ lotics app deploy -m "…" # pushes pending bindings, then ships
223
237
  ```
224
238
 
225
239
  A deploy pushes any workflow body, agent prose or query that is ahead of the app **before** it
226
- ships the bundle, so forgetting a `set` no longer ships a bundle typed against a binding that does
227
- not exist. `lotics app check` reports the same set without pushing.
240
+ ships the bundle, so a forgotten `set` cannot ship a bundle typed against a binding that does not
241
+ exist. `lotics app check` reports the same set without pushing.
228
242
 
229
243
  Keep the CLI current: an old one silently drops manifest fields it does not model.
@@ -37,7 +37,7 @@ Per-command syntax, flags, contracts, and gotchas for the public `lotics` CLI. S
37
37
  | `lotics upgrade` | Take the installed package's next version for the app in this directory (`app_id` from the local manifest — and the WORKSPACE from the same manifest, exactly as `lotics app *` does, so the command that knows which workspace it belongs to never rides the ambient profile). Announces its target on stderr before acting. **Applies the version it PREVIEWED**, not "latest" re-resolved server-side, so a release landing mid-command cannot install a contract whose diff was never checked. **Previews first and applies only a CLEAN upgrade**: a breaking contract change, a locally modified artifact, binding drift, or bundled knowledge needing consent (all FOUR sources the preview returns — the server throws for each, so omitting one only swapped the framed refusal for a raw wire 400) is reported per item and REFUSED with exit 1, because each resolves by choosing what to keep and a guess discards work nobody asked to lose. Already-current is a no-op that says so. On success it names the new version and the changelog, and points at `lotics app pull` to bring the checkout in step. The resolutions flow for a conflicted upgrade stays in `opctl` — that case needs a person, and the person is an operator. |
38
38
  | `lotics docs` \| `lotics docs <area>` | The index of the reference docs, **resolved out of the packages installed beside this project** — never carried by this CLI. **Both levels are discovered by looking**: every `@lotics/*` package carrying an `AGENTS.md` or a `docs/` in any `node_modules/@lotics` from the current directory UPWARD (nearest wins, so a hoisted root copy never shadows the one a project's own imports resolve to — checking only cwd made an agent editing `src/App.tsx`, or anyone on a workspaces/pnpm layout, see app-sdk and ui vanish while the `@lotics/cli` fallback kept the index looking complete) (six do today — app-sdk, ui, cli, docx, ooxml, xlsx), and within each, every area it actually ships. Titles come from each file's own `# heading` and the version from the installed `package.json`, so a doc OR a whole package added upstream appears with no change to this CLI, and a skewed install is visible rather than reassuring. A package's index is named after the package (`lotics docs ui`), never `index` — every package has one, so a shared name would make the most obvious query the one guaranteed to be ambiguous. `@lotics/app-sdk`, `@lotics/ui` and `@lotics/cli` sort first as a reading ORDER, not a filter. Bundling them here instead would print the contract of whichever kit version this CLI was built against — the kit publishes several times a day — and serving them from an endpoint would answer `latest` to a project on a pin: both are current-and-wrong, which is the one failure npm already prevents. Both the index and `<area>` print to **stdout** — the index is the payload of a bare `lotics docs`, so `lotics docs | grep -i excel` works — with only the provenance line on stderr, so `lotics docs ai > ai.md` is the doc alone; a name two packages share is refused with both qualified forms (`lotics docs ui/templates`) rather than resolved silently. Needs no auth — the references describe the product, not a workspace, so a key that will not resolve is exactly when someone reaches for them. Outside a project only `@lotics/cli`'s own resolve, and it says so. |
39
39
  | `lotics report '<json>'` \| `lotics report @report.json` | File a report with the Lotics team about what got in your way. **Covers the classes telemetry structurally cannot see**: a capability that does not exist (no command ran, so nothing was recorded), a command that exited 0 having done the wrong thing, an error whose message did not name the remedy, and anything that made authoring slower than it should be. **A frame, not a paragraph** — `{goal, actual, expected?, tried?, wanted?}`, `goal` and `actual` required, unknown keys dropped rather than refused. The frame exists because a session join reconstructs what was RUN and can never reconstruct what was WANTED: "the deploy warned twice" is unactionable, the same line under a goal names the defect. Each field becomes its own log attribute, so `goal` clusters across sessions instead of being grepped out of prose. **No severity or category** — those are judgements rather than facts, derivable at analysis time (a report followed by more commands on the same task shows a workaround; one followed by nothing shows a dead end), and a taxonomy shipped in a released binary cannot be corrected without a release. Ingest is inline JSON, `@file`, or `-` for stdin. The explicit `-` is the one place this departs from `run`, and it has to: `run` reaches stdin only after a tool name, while a report's single positional IS the payload, so bare and piped are the same argv. `isTTY` cannot separate them — it is falsy for a pipe a supervisor holds open and never writes to — so guessing hangs the bare form forever, and a grace period instead drops the report of any writer slower than the timer. A bare sentence is refused with the frame printed beside it, so the fix is one step; a bare invocation prints the frame BEFORE asking for a credential, since someone whose key will not resolve is exactly who has something to report. **Not spooled**: unlike telemetry it posts inline, prints whether it landed, and exits non-zero if it did not, echoing the report back so a failed send never loses it. Runs regardless of `LOTICS_TELEMETRY` — invoking it IS the consent that passive collection needs an opt-in for — but with telemetry off there are no recorded commands to attach, and it says so rather than implying context it does not have. Requires auth. Never paste records, file contents, or credentials. |
40
- | `lotics app check` | Every pre-flight `deploy` runs, WITHOUT building or shipping: the manifest's agent schemas against the live app row, every binding a deploy would push, aliases the source calls that nothing bound (queries, workflows AND agents), bindings the app serves that the source names nowhere, capability-gated SDK calls the manifest doesn't declare, a missing icon/theme, a `vite.config.ts` that never defines `global`/`__DEV__` and a `window.open` in the app's own source (both fail ONLY in the deployed app — dev bundles with esbuild and production with rollup, so typecheck, lint, build and `app dev` are all green while react-native-web reads `global.cancelAnimationFrame` as a free variable and the sandboxed iframe drops a popup silently), an agent holding `run_app_query`/`run_app_workflow` with an EMPTY `query_aliases`/`workflow_aliases` (the tool is the capability, the alias list is the reach — empty means every call it makes is refused while the run still COMPLETES, so it surfaces as a model ignoring its prompt; read off the live row, never the manifest, which mirrors those fields but is pushed by no verb), and a notice for any alias the source computes at runtime (invisible to every check here and to the deploy's unbind guard). Adds no rule of its own — each finding is the same helper `deploy` calls, so a green check means a deploy will not complain. **Exits 1 on what a `deploy` would REFUSE or PUSH** — an agent schema that disagrees with the live app, and any binding the project has ahead of the app (an edited workflow body or declaration, edited agent prose, a changed query). Both are things a deploy would act on, so CI gating on a green check means a deploy has nothing left to do; genuine advisories (capabilities, branding, a runtime-computed alias, orphaned bindings) stay advisory and never fail it. The point is the question being ASKABLE: these checks used to cost a build, a tar, an upload and a version row in the audit trail, which is expensive enough that the honest move was to skip them and find out in production. |
40
+ | `lotics app check` | Every pre-flight `deploy` runs, WITHOUT building or shipping: the manifest's agent schemas against the live app row, every binding a deploy would push, aliases the source calls that nothing bound (queries, workflows AND agents), bindings the app serves that the source names nowhere, capability-gated SDK calls the manifest doesn't declare, a missing icon/theme, a missing app `description` (it heads the capability catalog the chat agent reads every turn, and its absence has no other symptom), a `vite.config.ts` that never defines `global`/`__DEV__` and a `window.open` in the app's own source (both fail ONLY in the deployed app — dev bundles with esbuild and production with rollup, so typecheck, lint, build and `app dev` are all green while react-native-web reads `global.cancelAnimationFrame` as a free variable and the sandboxed iframe drops a popup silently), an agent holding `run_app_query`/`run_app_workflow` with an EMPTY `query_aliases`/`workflow_aliases` (the tool is the capability, the alias list is the reach — empty means every call it makes is refused while the run still COMPLETES, so it surfaces as a model ignoring its prompt; read off the live row, never the manifest, which mirrors those fields but is pushed by no verb), and a notice for any alias the source computes at runtime (invisible to every check here and to the deploy's unbind guard). Adds no rule of its own — each finding is the same helper `deploy` calls, so a green check means a deploy will not complain. **Exits 1 on what a `deploy` would REFUSE or PUSH** — an agent schema that disagrees with the live app, and any binding the project has ahead of the app (an edited workflow body or declaration, edited agent prose, a changed query). Both are things a deploy would act on, so CI gating on a green check means a deploy has nothing left to do; genuine advisories (capabilities, branding, a runtime-computed alias, orphaned bindings) stay advisory and never fail it. The point is the question being ASKABLE: these checks used to cost a build, a tar, an upload and a version row in the audit trail, which is expensive enough that the honest move was to skip them and find out in production. |
41
41
  | `lotics app workflow run <alias> '<json>'` | Execute a bound app workflow end-to-end via `appWorkflow`. `app_id` comes from the local manifest; the alias must be bound (`set_app_workflow`). Inputs ingest exactly like `lotics run` (inline JSON / `@file` / stdin — bulk inputs bypass `ARG_MAX`). Prints the full `{status,message,data,files,side_effects}` JSON to stdout + a one-line summary to stderr; exits non-zero on `status:"error"` (assertable). `--print-created` (alias `--report-effects`) renders the honest post-run harvest: created records grouped by table, a paste-ready `lotics run delete_records …` per table, then the **mandatory caveat** naming what cannot be auto-undone (external integrations + notifications) and that sub-workflows may have run. `--cleanup` (DEFAULT OFF, implies the report) additionally runs the deletes for harvested records ONLY — never files / external / notifications. Neither is a rollback — a rollback is structurally impossible here. |
42
42
  | `lotics app workflow set <alias>` | Push the edited `src/workflows/<alias>.ts` body through `set_app_workflow` (the single author of `apps.workflows`). Reads the body from disk (header + `/// <reference>` + `export {};` marker + the `__workflow` wrapper all stripped) + the typed `inputs`/`outputs` **and the `description`** from `package.json#lotics.workflows.<alias>`; the **server** re-verifies the body and echoes the bound `outputs` (declared, else DERIVED from `return({ data })`). The `description` is the one line an agent reads when choosing between the app's aliases (the workflow counterpart to a query's) — authored in the manifest so it lives beside the body in version control and rides every push; omit it and the workflow keeps whatever description it already has, so a push can never blank one set elsewhere. When the manifest declared NO `outputs`, the DERIVED echo is written back into `package.json#lotics.workflows.<alias>.outputs` (a SURGICAL write — preserves `knowledge`/`config` and every other manifest field) and that alias's types are refreshed in place, so `useWorkflow("<alias>")`'s `result.data` is typed immediately with no hand-copy and no second `lotics app codegen`; an explicitly-declared `outputs` is authoritative and never overwritten. Deploy still never authors workflows — this is a CLI convenience over the existing tool. Clear error + non-zero exit on a missing file, an alias absent from the manifest, or a verify failure. |
43
43
  | `lotics app agent set <alias>` | Push `src/agents/<alias>.md` — plus `inputs`/`outputs` when `package.json#lotics.agents.<alias>` declares them — through `set_app_agent`. The agent mirror of `app workflow set`, and the deploy-free authoring path for an agent's prose and its typed edges. **It sends only those fields.** Everything else is absent, and absent means unchanged, so a declaration this CLI does not model cannot be reverted by a push from a checkout that predates it — the chat authoring agent's `knowledge_doc_ids`, another operator's `query_aliases` grant. To change one of those, call `set_app_agent` with just that field (`lotics run set_app_agent '{"app_id":…,"alias":…,"tool_names":[…]}'` — it merges), then `app pull` to bring the manifest back in step. **CREATES the alias when the app has not bound one yet**, so a new agent is authored the same way a new workflow is: write the prose, declare the typed half, push. A create needs the prose file (an agent without instructions is not an agent); it is gated on nothing else, because what keeps a binding alive is a `useAppAgentRun("<alias>")` call site in the shipped bundle — a deploy prunes an agent the bundle never names, manifest entry or not. The prose push is a conditional write against the fingerprint this project last saw, so it is refused rather than allowed to overwrite prose someone else changed. Clear error + non-zero exit when there is no prose file and nothing declared to push instead, when a create has no prose to create from, or when the file is empty once the header is stripped. |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotics/cli",
3
- "version": "0.143.0",
3
+ "version": "0.143.2",
4
4
  "description": "Lotics SDK and CLI for AI agents",
5
5
  "type": "module",
6
6
  "bin": {