@lotics/cli 0.143.0 → 0.143.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md CHANGED
@@ -1,9 +1,7 @@
1
1
  # @lotics/cli — agent index
2
2
 
3
3
  The model an agent needs before driving this CLI: which surface answers which question, what the
4
- conventions are, and where the traps are. It deliberately does **not** list commands — `lotics --help`
5
- renders § COMMANDS from the same table the CLI dispatches on, so it is the one inventory that cannot
6
- fall behind the binary you have.
4
+ conventions are, and where the traps are.
7
5
 
8
6
  | Read | For |
9
7
  |---|---|
@@ -51,18 +49,15 @@ whether something exists, read `lotics --help` § COMMANDS — the whole section
51
49
  `workspace doctor` on findings. Scripts can gate on them.
52
50
  - **Text output is the default and is built for reading**; reach for `--json` only when a field is
53
51
  needed programmatically.
54
- - **`lotics report '<json>'` is the channel for what nothing else records.** Telemetry is keyed off
55
- commands that FAILED, so the two classes worth the most are invisible to it: a capability that
56
- does not exist (no command ran) and a run that exited 0 having done the wrong thing. Reach for it
57
- when the platform is genuinely missing something, when a success was wrong, or when an error did
52
+ - **`lotics report '<json>'` is the channel for what nothing else records.** Reach for it
53
+ when the platform is genuinely missing something (a capability that does not exist no
54
+ command ran), when a success was wrong (exited 0, wrong effect), or when an error did
58
55
  not name the remedy — not for your own mistakes, which the logs already show.
59
56
  **It is a frame, not a paragraph** — `{goal, actual, expected?, tried?, wanted?}`, because a log
60
57
  reconstructs what you RAN and never what you WANTED, and that gap is the report. `goal` and
61
- `actual` are required; there is no severity or category to pick (how bad and how common are read
62
- off the corpus, and a taxonomy in a released binary cannot be corrected without a release). The
58
+ `actual` are required; there is no severity or category to pick. The
63
59
  session's commands attach themselves — do not retype them. Run it bare for the full prompt; a
64
- long one rides `@file.json` or an explicit `-` for stdin (bare NEVER reads stdin, so an
65
- inherited pipe cannot wedge the one command meant to unblock you).
60
+ long one rides `@file.json` or an explicit `-` for stdin (bare NEVER reads stdin).
66
61
  - **`LOTICS_TELEMETRY=1` correlates a whole session** so the authoring loop's rough edges can be
67
62
  found and fixed. Off by default; set it in the shell profile, not per command (each invocation is
68
63
  its own process). It sends no arguments, no file contents, and no record data — see README
@@ -93,7 +88,3 @@ whether something exists, read `lotics --help` § COMMANDS — the whole section
93
88
  `src/agents/<alias>.md` and is pushed by `app agent set`.
94
89
  - **OAuth connections.** Attaching a connected account is web-only; the CLI can list them.
95
90
  - **Anything needing a browser.** `app dev` and `file preview` shell out to a local Chrome.
96
-
97
- Everything above is a *known* boundary. When you hit one that is NOT listed here — a verb that
98
- should exist and does not — that is the gap worth reporting, and `lotics report` is how it reaches
99
- us. Working around it silently is how it stays missing.
package/README.md CHANGED
@@ -23,9 +23,12 @@ time, so a doc or a whole package added upstream shows up without upgrading this
23
23
  Per-command contracts, flags, exit codes and gotchas are in
24
24
  [docs/cli_reference.md](./docs/cli_reference.md); `lotics --help` is the always-current verb list.
25
25
 
26
- Two of the platform's primary surfaces have dedicated usage guides that ship inside this
26
+ The platform's primary surfaces have dedicated usage guides that ship inside this
27
27
  package (reachable at `node_modules/@lotics/cli/docs/*.md` once installed):
28
28
 
29
+ - [`docs/building_an_app.md`](docs/building_an_app.md) — building a custom-code app end to end:
30
+ the sequence the steps go in (clarify → model → types → queries → workflows → screens → ship)
31
+ and the deploy-free inner loop. Read it once before starting an app.
29
32
  - [`docs/document_templates.md`](docs/document_templates.md) — generate finished documents
30
33
  (PDF, Excel, Word, email) by filling reusable templates: the five template types, the
31
34
  create → generate → chain lifecycle, and the marker capabilities.
@@ -33,8 +36,6 @@ package (reachable at `node_modules/@lotics/cli/docs/*.md` once installed):
33
36
  workspace facts an agent can't guess, the access-vs-activation model, and the
34
37
  catalog-then-stage retrieval model agents use to pull only the lines they need.
35
38
 
36
- Both point to `lotics tools <name>` for exact input schemas.
37
-
38
39
  ## Install
39
40
 
40
41
  ```bash
@@ -118,7 +119,7 @@ Every request identifies the CLI (`user-agent: lotics-cli/<version> node/<v> <pl
118
119
  - Requests carry a session id shared by every command in the sitting — under an agent harness it adopts the harness's own session id, otherwise it rolls over after 30 minutes idle (`~/.lotics/session.json`).
119
120
  - Each invocation appends one record to `~/.lotics/telemetry.ndjson` — the command, its exit code, how long it took, and, on a failure, the message that was printed. Batches are sent on a later run; a failed send is retried, never dropped silently.
120
121
 
121
- Arguments contribute a **hash** and a **shape** (`records[].data.name:string`) and nothing else — no values, no file contents, no record data. The hash is the point: two failures in a row with the same hash mean the error message did not tell you enough to fix the call, which is how bad errors get found. This turns "this command failed" into "this command failed after these eleven, and the agent never recovered."
122
+ Arguments contribute a **hash** and a **shape** (`records[].data.name:string`) and nothing else — no values, no file contents, no record data. The hash is the point: two failures in a row with the same hash mean the error message did not tell you enough to fix the call.
122
123
 
123
124
  Unset, nothing is stored, nothing is sent, and no spool file is created.
124
125
 
@@ -148,14 +149,10 @@ reconstruct what you *wanted*, and the gap between the two is the report.
148
149
  | `tried` | | what you already tried, and what it said |
149
150
  | `wanted` | | what would have unblocked you |
150
151
 
151
- There is no severity or category to pick. How bad and how common are read off the corpus; what you
152
- were trying to do is not recoverable from anywhere else.
152
+ There is no severity or category to pick.
153
153
 
154
- Reading from stdin needs an explicit `-`. `report`'s only argument IS the report, so a bare
155
- invocation and a piped one look identical in argv and "is this a pipe?" cannot tell a live
156
- writer from a descriptor a supervisor left open, so guessing either hangs forever or drops a
157
- slow writer's report. With `-` the bare form always prints the frame and a piped one waits as
158
- long as it takes.
154
+ Reading from stdin needs an explicit `-`. With `-` the bare form always prints the frame and a
155
+ piped one waits as long as it takes.
159
156
 
160
157
  If `LOTICS_TELEMETRY=1` is set, the commands from your session attach themselves — don't retype
161
158
  them. It runs whether or not telemetry is on (invoking it is the consent that passive recording
@@ -163,7 +160,7 @@ needs an opt-in for), posts immediately, and tells you if it did not land.
163
160
 
164
161
  Do not paste records, file contents, or credentials.
165
162
 
166
- In an app project, `lotics app *` commands derive the credential from the directory when nothing explicit chose one: the manifest names the app's workspace, and when exactly one saved profile owns that workspace, that profile is used — the machine-wide default (which another shell can move between two of your commands) is never consulted. An explicit flag, env var, or directory pin still wins, and an org whose profile remembers a different workspace simply falls through to the announced default, exactly as before.
163
+ In an app project, `lotics app *` commands derive the credential from the directory when nothing explicit chose one: the manifest names the app's workspace, and when exactly one saved profile owns that workspace, that profile is used — the machine-wide default is never consulted. An explicit flag, env var, or directory pin still wins, and an org whose profile remembers a different workspace simply falls through to the announced default.
167
164
 
168
165
  ## Workspaces
169
166
 
@@ -313,7 +310,6 @@ cat input.json | lotics app agent run app_abc recognize # inputs via stdi
313
310
  lotics app agent run app_abc recognize --json # full run summary to stdout
314
311
  lotics app agent run app_abc recognize --session cli-123 '{}' # continue an existing thread
315
312
 
316
- # Dev-link @lotics/ui to packages/ui/src for live HMR (Vite alias; deploy bundles it)
317
313
  # Dev-link @lotics/ui to a monorepo checkout for ONE command — nothing is written to disk
318
314
  LOTICS_UI_SRC=/abs/monorepo/packages/ui/src lotics app dev
319
315
  LOTICS_UI_SRC=/abs/monorepo/packages/ui/src lotics app deploy -m "..." # warns: bundles YOUR kit copy
@@ -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
  }