solve-engine 2.38.5 → 2.38.7

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.
Files changed (90) hide show
  1. package/dist/{PackageCompatibility-CuUVgjBW.d.cts → PackageCompatibility-rJfOPkuE.d.cts} +1 -1
  2. package/dist/{PackageCompatibility-DscVGabN.d.ts → PackageCompatibility-th5IHCuQ.d.ts} +1 -1
  3. package/dist/{PackageRegistry-G6qyHROU.d.cts → PackageRegistry-CDZch8VI.d.cts} +79 -1
  4. package/dist/{PackageRegistry-DJekaRdM.d.ts → PackageRegistry-yDMyorr3.d.ts} +79 -1
  5. package/dist/{VMBuiltins-BnE1KdV5.d.ts → VMBuiltins-CjuRXAOD.d.ts} +54 -6
  6. package/dist/{VMBuiltins-RpmR0yHh.d.cts → VMBuiltins-DR0PyI_g.d.cts} +54 -6
  7. package/dist/{VMCheckpoints-DpHlGjJ3.d.ts → VMCheckpoints-C2REEAxr.d.ts} +1 -1
  8. package/dist/{VMCheckpoints-D0_5-IIa.d.cts → VMCheckpoints-Ch3ElC4X.d.cts} +1 -1
  9. package/dist/chunk-4LSPMT2M.js +3 -0
  10. package/dist/chunk-4LSPMT2M.js.map +1 -0
  11. package/dist/chunk-62JRBFBM.cjs +3 -0
  12. package/dist/chunk-62JRBFBM.cjs.map +1 -0
  13. package/dist/{chunk-YXULZQCL.cjs → chunk-6ONIPE5L.cjs} +3 -3
  14. package/dist/{chunk-YXULZQCL.cjs.map → chunk-6ONIPE5L.cjs.map} +1 -1
  15. package/dist/chunk-AAUDBEC2.cjs +5 -0
  16. package/dist/chunk-AAUDBEC2.cjs.map +1 -0
  17. package/dist/{chunk-GIV7RB5F.cjs → chunk-CNOYGBBV.cjs} +2 -2
  18. package/dist/{chunk-GIV7RB5F.cjs.map → chunk-CNOYGBBV.cjs.map} +1 -1
  19. package/dist/chunk-JJ4BY6X6.cjs +2 -0
  20. package/dist/chunk-JJ4BY6X6.cjs.map +1 -0
  21. package/dist/chunk-KL2F3YIQ.js +2 -0
  22. package/dist/chunk-KL2F3YIQ.js.map +1 -0
  23. package/dist/{chunk-G6KM25AA.js → chunk-LZFH7CWS.js} +2 -2
  24. package/dist/{chunk-G6KM25AA.js.map → chunk-LZFH7CWS.js.map} +1 -1
  25. package/dist/chunk-QFZYS7CY.cjs +2 -0
  26. package/dist/chunk-QFZYS7CY.cjs.map +1 -0
  27. package/dist/chunk-RRI3ZOI3.js +2 -0
  28. package/dist/chunk-RRI3ZOI3.js.map +1 -0
  29. package/dist/chunk-UV6H33IZ.cjs +2 -0
  30. package/dist/chunk-UV6H33IZ.cjs.map +1 -0
  31. package/dist/chunk-WP2CO6BC.js +3 -0
  32. package/dist/chunk-WP2CO6BC.js.map +1 -0
  33. package/dist/chunk-X7556JAZ.js +2 -0
  34. package/dist/chunk-X7556JAZ.js.map +1 -0
  35. package/dist/chunk-Z4KZETIJ.js +5 -0
  36. package/dist/chunk-Z4KZETIJ.js.map +1 -0
  37. package/dist/constants.cjs +1 -1
  38. package/dist/constants.js +1 -1
  39. package/dist/engine.cjs +1 -1
  40. package/dist/engine.d.cts +12 -4
  41. package/dist/engine.d.ts +12 -4
  42. package/dist/engine.js +1 -1
  43. package/dist/engine.worker.cjs +1 -1
  44. package/dist/engine.worker.js +1 -1
  45. package/dist/engine.worker.js.map +1 -1
  46. package/dist/index.cjs +1 -1
  47. package/dist/index.d.cts +4 -4
  48. package/dist/index.d.ts +4 -4
  49. package/dist/index.js +1 -1
  50. package/dist/language.d.cts +3 -3
  51. package/dist/language.d.ts +3 -3
  52. package/dist/packages.cjs +1 -1
  53. package/dist/packages.d.cts +2 -2
  54. package/dist/packages.d.ts +2 -2
  55. package/dist/packages.js +1 -1
  56. package/dist/testing.cjs +2 -2
  57. package/dist/testing.d.cts +3 -3
  58. package/dist/testing.d.ts +3 -3
  59. package/dist/testing.js +1 -1
  60. package/dist/vm.cjs +1 -1
  61. package/dist/vm.d.cts +2 -2
  62. package/dist/vm.d.ts +2 -2
  63. package/dist/vm.js +1 -1
  64. package/dist/worker.cjs +2 -2
  65. package/dist/worker.d.cts +2 -2
  66. package/dist/worker.d.ts +2 -2
  67. package/dist/worker.js +1 -1
  68. package/package.json +1 -1
  69. package/dist/chunk-BLTGR5AX.cjs +0 -5
  70. package/dist/chunk-BLTGR5AX.cjs.map +0 -1
  71. package/dist/chunk-D52DE6EY.js +0 -3
  72. package/dist/chunk-D52DE6EY.js.map +0 -1
  73. package/dist/chunk-GOHW34BB.js +0 -2
  74. package/dist/chunk-GOHW34BB.js.map +0 -1
  75. package/dist/chunk-LZUPPER4.js +0 -5
  76. package/dist/chunk-LZUPPER4.js.map +0 -1
  77. package/dist/chunk-M4UC2PB6.cjs +0 -2
  78. package/dist/chunk-M4UC2PB6.cjs.map +0 -1
  79. package/dist/chunk-MSOMMARC.cjs +0 -3
  80. package/dist/chunk-MSOMMARC.cjs.map +0 -1
  81. package/dist/chunk-O7IUJXOT.cjs +0 -2
  82. package/dist/chunk-O7IUJXOT.cjs.map +0 -1
  83. package/dist/chunk-QXKF3QOX.js +0 -3
  84. package/dist/chunk-QXKF3QOX.js.map +0 -1
  85. package/dist/chunk-TJKN466F.js +0 -2
  86. package/dist/chunk-TJKN466F.js.map +0 -1
  87. package/dist/chunk-UDBOAKLW.js +0 -2
  88. package/dist/chunk-UDBOAKLW.js.map +0 -1
  89. package/dist/chunk-WCYAFAUN.cjs +0 -2
  90. package/dist/chunk-WCYAFAUN.cjs.map +0 -1
@@ -1,4 +1,4 @@
1
- import { I as IEnginePackage } from './PackageRegistry-G6qyHROU.cjs';
1
+ import { I as IEnginePackage } from './PackageRegistry-CDZch8VI.cjs';
2
2
 
3
3
  /**
4
4
  * Package load-time compatibility checking, the "detect overlapping
@@ -1,4 +1,4 @@
1
- import { I as IEnginePackage } from './PackageRegistry-DJekaRdM.js';
1
+ import { I as IEnginePackage } from './PackageRegistry-yDMyorr3.js';
2
2
 
3
3
  /**
4
4
  * Package load-time compatibility checking, the "detect overlapping
@@ -2,7 +2,7 @@ import { a as PrecedenceParser, b as PrefixParselet, I as InfixParselet } from '
2
2
  import { M as MarkdownLineType, L as Lexer, e as TokenCategory, c as LexerVocabulary } from './Lexer-DaWkCu84.cjs';
3
3
  import { IAsyncResolver } from './resolvers.cjs';
4
4
  import { T as TokenFusion, c as TokenNormalizer, a as NormalizerRule } from './TokenNormalizer-D382VSYh.cjs';
5
- import { D as DependencyGraph, V as VM, f as DagSnapshot, g as EngineContext, S as ScopeManager, P as PluginFunctionHandler, A as AsConverter } from './VMBuiltins-RpmR0yHh.cjs';
5
+ import { D as DependencyGraph, V as VM, f as DagSnapshot, g as EngineContext, S as ScopeManager, P as PluginFunctionHandler, A as AsConverter } from './VMBuiltins-DR0PyI_g.cjs';
6
6
  import { V as Value, b as ValueType, D as DatetimeGrain } from './Value-B55Hvn3e.cjs';
7
7
  import { a as BytecodeProgram } from './BytecodeBuilder-DSWKZi4f.cjs';
8
8
  import { C as CalendarBackend } from './CalendarBackend-MdS3Bb4P.cjs';
@@ -261,6 +261,23 @@ declare class DocumentModel {
261
261
  * bottom of a 20k-line document before this fix.
262
262
  */
263
263
  private dirtyLineIds;
264
+ /**
265
+ * Lower-cased category tag to the ids of the lines carrying it, or `null`
266
+ * while nothing has asked.
267
+ *
268
+ * `total of #food` used to walk the whole document looking for `#food`, so a
269
+ * document of members and totals cost one walk per total: D aggregates over
270
+ * N lines is D x N, and a notepad of a few thousand tagged lines took
271
+ * minutes. This turns an aggregate into a lookup of its own members.
272
+ *
273
+ * Built on the first query rather than with the document, so a document with
274
+ * no tags in it, which is most of them, never scans for any. Once built it is
275
+ * kept in step by the four places that change a line's text: {@link setDocument},
276
+ * {@link applyChanges}, {@link editLine} and {@link clear}. Keyed by line id
277
+ * rather than position, so inserting or deleting a line above a tagged one
278
+ * does not touch it.
279
+ */
280
+ private tagIndex;
264
281
  /**
265
282
  * Most lines this model will hold. See `constants/Configuration.ts`'s
266
283
  * `performance.maxDocumentLines`, which is where the default comes from and
@@ -320,6 +337,27 @@ declare class DocumentModel {
320
337
  * Returns true if the text actually changed (hash mismatch).
321
338
  */
322
339
  editLine(lineNumber: number, newText: string): boolean;
340
+ /**
341
+ * The 1-based positions of the lines carrying `#tag`, in document order.
342
+ *
343
+ * What `total of #tag` reads. Case-insensitive, matching how a tag is read
344
+ * on a line, and it names membership only: a line that merely asks about the
345
+ * group (`total of #tag`) is not in it, and neither is a `#heading`.
346
+ *
347
+ * Ascending, because an aggregate reports the first line it cannot read and
348
+ * the line it names has to be the first one in the document, not whichever
349
+ * happened to be indexed first.
350
+ *
351
+ * @param tag - The tag name without its `#`.
352
+ * @returns Its lines' positions, ascending; empty when no line carries it.
353
+ */
354
+ linesCarryingTag(tag: string): number[];
355
+ /** The tag index, built over every line the first time one is asked for. */
356
+ private ensureTagIndex;
357
+ /** Records `lineId` as a member of every group its text joins. */
358
+ private indexTags;
359
+ /** Drops `lineId` from every group the text it used to hold joined. */
360
+ private unindexTags;
323
361
  /**
324
362
  * Get the LineState at the given 1-based line position. O(1).
325
363
  */
@@ -1960,6 +1998,16 @@ declare class ExpressionEngine {
1960
1998
  */
1961
1999
  private batchScanResults;
1962
2000
  private batchParsedLines;
2001
+ /**
2002
+ * Lower-cased category tag to the 1-based lines carrying it, for the batch
2003
+ * pass, or `null` before anything has asked.
2004
+ *
2005
+ * The batch path has no document model to hold this, and the scan it does
2006
+ * have lives only for the pass, so the index does too: built over the scan
2007
+ * on the first aggregate and dropped with it. A document with no aggregate
2008
+ * in it never builds one.
2009
+ */
2010
+ private batchTagIndex;
1963
2011
  /**
1964
2012
  * The context handed to every line of the current pass, its line number
1965
2013
  * set by mutation before each execution. The closures inside read only
@@ -2378,6 +2426,23 @@ declare class ExpressionEngine {
2378
2426
  * still cancels it), or null when nothing was linked.
2379
2427
  */
2380
2428
  private armCancellation;
2429
+ /**
2430
+ * Register a line's edges, including the groups its text joins and asks about.
2431
+ *
2432
+ * A category tag is an edge like any other: a line carrying `#food` writes
2433
+ * that group, and `total of #food` reads it. Registering them here is what
2434
+ * lets an edit to a tagged line dirty only the aggregates over that tag.
2435
+ *
2436
+ * Membership is a property of the text rather than of the bytecode, so it
2437
+ * comes from the same scanner the aggregate itself reads with, which is what
2438
+ * keeps the two from ever disagreeing about what is in a group.
2439
+ *
2440
+ * @param lineNumber - 1-based line the edges belong to.
2441
+ * @param text - The line's expression, which is where its tags are.
2442
+ * @param reads - Variable keys the line reads.
2443
+ * @param writes - Variable keys the line writes.
2444
+ */
2445
+ private registerLineWithTags;
2381
2446
  /**
2382
2447
  * Execute bytecode and handle the result.
2383
2448
  *
@@ -2455,6 +2520,19 @@ declare class ExpressionEngine {
2455
2520
  * bypassing the split→join roundtrip that this method performs.
2456
2521
  */
2457
2522
  evaluateLines(lines: string[]): ParsedLine[];
2523
+ /**
2524
+ * The lines of the batch pass's scan carrying `#tag`, ascending.
2525
+ *
2526
+ * The batch counterpart of `DocumentModel.linesCarryingTag`, over the scan
2527
+ * this pass is walking rather than over a document model. Built once and
2528
+ * read by every aggregate in the pass, which is what makes the pass linear
2529
+ * in the document rather than linear per aggregate.
2530
+ *
2531
+ * @param scan - The pass's scanned lines, indexed from 0 for line 1.
2532
+ * @param tag - The tag name without its `#`.
2533
+ * @returns Its lines' 1-based positions, ascending; empty when none carry it.
2534
+ */
2535
+ private batchLinesCarryingTag;
2458
2536
  /**
2459
2537
  * Process pre-scanned line results into ParsedLine objects.
2460
2538
  *
@@ -2,7 +2,7 @@ import { a as PrecedenceParser, b as PrefixParselet, I as InfixParselet } from '
2
2
  import { M as MarkdownLineType, L as Lexer, e as TokenCategory, c as LexerVocabulary } from './Lexer-DPn5dfCH.js';
3
3
  import { IAsyncResolver } from './resolvers.js';
4
4
  import { T as TokenFusion, c as TokenNormalizer, a as NormalizerRule } from './TokenNormalizer-DAzPp-j1.js';
5
- import { D as DependencyGraph, V as VM, f as DagSnapshot, g as EngineContext, S as ScopeManager, P as PluginFunctionHandler, A as AsConverter } from './VMBuiltins-BnE1KdV5.js';
5
+ import { D as DependencyGraph, V as VM, f as DagSnapshot, g as EngineContext, S as ScopeManager, P as PluginFunctionHandler, A as AsConverter } from './VMBuiltins-CjuRXAOD.js';
6
6
  import { V as Value, b as ValueType, D as DatetimeGrain } from './Value-B55Hvn3e.js';
7
7
  import { a as BytecodeProgram } from './BytecodeBuilder-DSWKZi4f.js';
8
8
  import { C as CalendarBackend } from './CalendarBackend-MdS3Bb4P.js';
@@ -261,6 +261,23 @@ declare class DocumentModel {
261
261
  * bottom of a 20k-line document before this fix.
262
262
  */
263
263
  private dirtyLineIds;
264
+ /**
265
+ * Lower-cased category tag to the ids of the lines carrying it, or `null`
266
+ * while nothing has asked.
267
+ *
268
+ * `total of #food` used to walk the whole document looking for `#food`, so a
269
+ * document of members and totals cost one walk per total: D aggregates over
270
+ * N lines is D x N, and a notepad of a few thousand tagged lines took
271
+ * minutes. This turns an aggregate into a lookup of its own members.
272
+ *
273
+ * Built on the first query rather than with the document, so a document with
274
+ * no tags in it, which is most of them, never scans for any. Once built it is
275
+ * kept in step by the four places that change a line's text: {@link setDocument},
276
+ * {@link applyChanges}, {@link editLine} and {@link clear}. Keyed by line id
277
+ * rather than position, so inserting or deleting a line above a tagged one
278
+ * does not touch it.
279
+ */
280
+ private tagIndex;
264
281
  /**
265
282
  * Most lines this model will hold. See `constants/Configuration.ts`'s
266
283
  * `performance.maxDocumentLines`, which is where the default comes from and
@@ -320,6 +337,27 @@ declare class DocumentModel {
320
337
  * Returns true if the text actually changed (hash mismatch).
321
338
  */
322
339
  editLine(lineNumber: number, newText: string): boolean;
340
+ /**
341
+ * The 1-based positions of the lines carrying `#tag`, in document order.
342
+ *
343
+ * What `total of #tag` reads. Case-insensitive, matching how a tag is read
344
+ * on a line, and it names membership only: a line that merely asks about the
345
+ * group (`total of #tag`) is not in it, and neither is a `#heading`.
346
+ *
347
+ * Ascending, because an aggregate reports the first line it cannot read and
348
+ * the line it names has to be the first one in the document, not whichever
349
+ * happened to be indexed first.
350
+ *
351
+ * @param tag - The tag name without its `#`.
352
+ * @returns Its lines' positions, ascending; empty when no line carries it.
353
+ */
354
+ linesCarryingTag(tag: string): number[];
355
+ /** The tag index, built over every line the first time one is asked for. */
356
+ private ensureTagIndex;
357
+ /** Records `lineId` as a member of every group its text joins. */
358
+ private indexTags;
359
+ /** Drops `lineId` from every group the text it used to hold joined. */
360
+ private unindexTags;
323
361
  /**
324
362
  * Get the LineState at the given 1-based line position. O(1).
325
363
  */
@@ -1960,6 +1998,16 @@ declare class ExpressionEngine {
1960
1998
  */
1961
1999
  private batchScanResults;
1962
2000
  private batchParsedLines;
2001
+ /**
2002
+ * Lower-cased category tag to the 1-based lines carrying it, for the batch
2003
+ * pass, or `null` before anything has asked.
2004
+ *
2005
+ * The batch path has no document model to hold this, and the scan it does
2006
+ * have lives only for the pass, so the index does too: built over the scan
2007
+ * on the first aggregate and dropped with it. A document with no aggregate
2008
+ * in it never builds one.
2009
+ */
2010
+ private batchTagIndex;
1963
2011
  /**
1964
2012
  * The context handed to every line of the current pass, its line number
1965
2013
  * set by mutation before each execution. The closures inside read only
@@ -2378,6 +2426,23 @@ declare class ExpressionEngine {
2378
2426
  * still cancels it), or null when nothing was linked.
2379
2427
  */
2380
2428
  private armCancellation;
2429
+ /**
2430
+ * Register a line's edges, including the groups its text joins and asks about.
2431
+ *
2432
+ * A category tag is an edge like any other: a line carrying `#food` writes
2433
+ * that group, and `total of #food` reads it. Registering them here is what
2434
+ * lets an edit to a tagged line dirty only the aggregates over that tag.
2435
+ *
2436
+ * Membership is a property of the text rather than of the bytecode, so it
2437
+ * comes from the same scanner the aggregate itself reads with, which is what
2438
+ * keeps the two from ever disagreeing about what is in a group.
2439
+ *
2440
+ * @param lineNumber - 1-based line the edges belong to.
2441
+ * @param text - The line's expression, which is where its tags are.
2442
+ * @param reads - Variable keys the line reads.
2443
+ * @param writes - Variable keys the line writes.
2444
+ */
2445
+ private registerLineWithTags;
2381
2446
  /**
2382
2447
  * Execute bytecode and handle the result.
2383
2448
  *
@@ -2455,6 +2520,19 @@ declare class ExpressionEngine {
2455
2520
  * bypassing the split→join roundtrip that this method performs.
2456
2521
  */
2457
2522
  evaluateLines(lines: string[]): ParsedLine[];
2523
+ /**
2524
+ * The lines of the batch pass's scan carrying `#tag`, ascending.
2525
+ *
2526
+ * The batch counterpart of `DocumentModel.linesCarryingTag`, over the scan
2527
+ * this pass is walking rather than over a document model. Built once and
2528
+ * read by every aggregate in the pass, which is what makes the pass linear
2529
+ * in the document rather than linear per aggregate.
2530
+ *
2531
+ * @param scan - The pass's scanned lines, indexed from 0 for line 1.
2532
+ * @param tag - The tag name without its `#`.
2533
+ * @returns Its lines' 1-based positions, ascending; empty when none carry it.
2534
+ */
2535
+ private batchLinesCarryingTag;
2458
2536
  /**
2459
2537
  * Process pre-scanned line results into ParsedLine objects.
2460
2538
  *
@@ -98,6 +98,16 @@ interface LineExecutionContext {
98
98
  calendar?: CalendarBackend;
99
99
  /** Look up another line's cached result by 1-based line number. `undefined` = not evaluated yet (or out of range), distinct from a line that evaluated to an actual `undefined`-like Value, which can't happen (every Value type has a concrete representation). */
100
100
  getLineResult?: (lineNumber: number) => Value | undefined;
101
+ /**
102
+ * The 1-based positions of the lines carrying `#tag`, ascending, or
103
+ * `undefined` when this path keeps no index and the caller should walk the
104
+ * document itself.
105
+ *
106
+ * `total of #tag` used to look at every line of the document, so a notepad
107
+ * of tagged amounts and totals cost aggregates x lines per pass. Both
108
+ * document paths maintain an index instead, and answer from it here.
109
+ */
110
+ getTaggedLines?: (tag: string) => readonly number[] | undefined;
101
111
  /** Whether line `lineNumber` is a blank line or a `#` heading, the stopping condition for "total above"/"sum above"/"average above" aggregation. */
102
112
  isLineBoundary?: (lineNumber: number) => boolean;
103
113
  /**
@@ -591,6 +601,8 @@ declare const sharedOpRegistry: OpRegistry;
591
601
  /** Serialized snapshot of the dependency graph for diagnostic rendering. */
592
602
  interface DagSnapshot {
593
603
  consumers: Record<string, number[]>;
604
+ /** key -> the lines that write it, the mirror of `consumers`. */
605
+ producers: Record<string, number[]>;
594
606
  writes: Record<number, string[]>;
595
607
  reads: Record<number, string[]>;
596
608
  dataSourceDeps: Record<number, string[]>;
@@ -607,12 +619,36 @@ interface DagSnapshot {
607
619
  * - Efficient removal of deleted lines (removeLine)
608
620
  */
609
621
  declare class DependencyGraph {
622
+ /** key -> the lines that READ it. Answers "what does an edit to this affect". */
610
623
  private consumers;
624
+ /**
625
+ * key -> the lines that WRITE it. Answers "what is in this group".
626
+ *
627
+ * The half that was missing. `consumers` alone can say which lines ask about
628
+ * `#food`; only this can say which lines are *in* it, which is what a
629
+ * category-tag aggregate needs and why it used to walk the whole document
630
+ * instead of asking.
631
+ */
632
+ private producers;
611
633
  private dependencies;
612
634
  private writes;
613
635
  private lineReads;
614
- private dataSourceDependencies;
615
- private dataSourceConsumers;
636
+ /**
637
+ * line -> the keys it reads that {@link registerLine} must not replace.
638
+ *
639
+ * Variable and tag edges are recovered from the line's text every time it is
640
+ * registered, so registering replaces them. A data-source edge is discovered
641
+ * at run time, after that registration, so replacing would drop it. Both live
642
+ * in the same {@link consumers} index; only the bookkeeping differs.
643
+ */
644
+ private pinnedReads;
645
+ /**
646
+ * Whether this line already carries exactly these edges.
647
+ *
648
+ * Reads are compared against the stored set minus its pinned keys, since a
649
+ * pinned key (a data source) is not part of what a caller passes.
650
+ */
651
+ private hasSameEdges;
616
652
  /**
617
653
  * Register a line's variable reads and writes in the dependency graph.
618
654
  *
@@ -668,7 +704,7 @@ declare class DependencyGraph {
668
704
  * @param queryKey - The query key that was updated
669
705
  * @returns Set of line numbers that need re-evaluation
670
706
  */
671
- getAffectedLinesByDataSource(dataSourceId: string, queryKey: string[]): Set<number>;
707
+ getAffectedLinesByDataSource(dataSourceId: string, queryKey: string[]): ReadonlySet<number>;
672
708
  /**
673
709
  * Remove a line from the dependency graph (e.g., when a line is deleted from the document).
674
710
  *
@@ -684,21 +720,33 @@ declare class DependencyGraph {
684
720
  * @param variable - The variable name
685
721
  * @returns Set of line numbers that read this variable, or empty set if none
686
722
  */
687
- getConsumers(variable: string): Set<number>;
723
+ getConsumers(variable: string): ReadonlySet<number>;
724
+ /**
725
+ * Get all line numbers that produce (write) a given key.
726
+ *
727
+ * The mirror of {@link getConsumers}, and the reason the producer index
728
+ * exists: for a category tag it is the group's membership, so an aggregate
729
+ * over `#food` costs the size of the group rather than the size of the
730
+ * document.
731
+ *
732
+ * @param key - The key, from {@link edgeKey}
733
+ * @returns Set of line numbers that write this key, or empty set if none
734
+ */
735
+ getProducers(key: string): ReadonlySet<number>;
688
736
  /**
689
737
  * Get all variables that a line depends on (reads).
690
738
  *
691
739
  * @param lineNumber - The line number to query
692
740
  * @returns Set of variable names this line reads, or empty set if none
693
741
  */
694
- getDependencies(lineNumber: number): Set<string>;
742
+ getDependencies(lineNumber: number): ReadonlySet<string>;
695
743
  /**
696
744
  * Get all variables that a line writes (assigns to).
697
745
  *
698
746
  * @param lineNumber - The line number to query
699
747
  * @returns Set of variable names this line writes, or empty set if none
700
748
  */
701
- getWrites(lineNumber: number): Set<string>;
749
+ getWrites(lineNumber: number): ReadonlySet<string>;
702
750
  /**
703
751
  * Get a serializable snapshot of the entire dependency graph for diagnostics.
704
752
  *
@@ -98,6 +98,16 @@ interface LineExecutionContext {
98
98
  calendar?: CalendarBackend;
99
99
  /** Look up another line's cached result by 1-based line number. `undefined` = not evaluated yet (or out of range), distinct from a line that evaluated to an actual `undefined`-like Value, which can't happen (every Value type has a concrete representation). */
100
100
  getLineResult?: (lineNumber: number) => Value | undefined;
101
+ /**
102
+ * The 1-based positions of the lines carrying `#tag`, ascending, or
103
+ * `undefined` when this path keeps no index and the caller should walk the
104
+ * document itself.
105
+ *
106
+ * `total of #tag` used to look at every line of the document, so a notepad
107
+ * of tagged amounts and totals cost aggregates x lines per pass. Both
108
+ * document paths maintain an index instead, and answer from it here.
109
+ */
110
+ getTaggedLines?: (tag: string) => readonly number[] | undefined;
101
111
  /** Whether line `lineNumber` is a blank line or a `#` heading, the stopping condition for "total above"/"sum above"/"average above" aggregation. */
102
112
  isLineBoundary?: (lineNumber: number) => boolean;
103
113
  /**
@@ -591,6 +601,8 @@ declare const sharedOpRegistry: OpRegistry;
591
601
  /** Serialized snapshot of the dependency graph for diagnostic rendering. */
592
602
  interface DagSnapshot {
593
603
  consumers: Record<string, number[]>;
604
+ /** key -> the lines that write it, the mirror of `consumers`. */
605
+ producers: Record<string, number[]>;
594
606
  writes: Record<number, string[]>;
595
607
  reads: Record<number, string[]>;
596
608
  dataSourceDeps: Record<number, string[]>;
@@ -607,12 +619,36 @@ interface DagSnapshot {
607
619
  * - Efficient removal of deleted lines (removeLine)
608
620
  */
609
621
  declare class DependencyGraph {
622
+ /** key -> the lines that READ it. Answers "what does an edit to this affect". */
610
623
  private consumers;
624
+ /**
625
+ * key -> the lines that WRITE it. Answers "what is in this group".
626
+ *
627
+ * The half that was missing. `consumers` alone can say which lines ask about
628
+ * `#food`; only this can say which lines are *in* it, which is what a
629
+ * category-tag aggregate needs and why it used to walk the whole document
630
+ * instead of asking.
631
+ */
632
+ private producers;
611
633
  private dependencies;
612
634
  private writes;
613
635
  private lineReads;
614
- private dataSourceDependencies;
615
- private dataSourceConsumers;
636
+ /**
637
+ * line -> the keys it reads that {@link registerLine} must not replace.
638
+ *
639
+ * Variable and tag edges are recovered from the line's text every time it is
640
+ * registered, so registering replaces them. A data-source edge is discovered
641
+ * at run time, after that registration, so replacing would drop it. Both live
642
+ * in the same {@link consumers} index; only the bookkeeping differs.
643
+ */
644
+ private pinnedReads;
645
+ /**
646
+ * Whether this line already carries exactly these edges.
647
+ *
648
+ * Reads are compared against the stored set minus its pinned keys, since a
649
+ * pinned key (a data source) is not part of what a caller passes.
650
+ */
651
+ private hasSameEdges;
616
652
  /**
617
653
  * Register a line's variable reads and writes in the dependency graph.
618
654
  *
@@ -668,7 +704,7 @@ declare class DependencyGraph {
668
704
  * @param queryKey - The query key that was updated
669
705
  * @returns Set of line numbers that need re-evaluation
670
706
  */
671
- getAffectedLinesByDataSource(dataSourceId: string, queryKey: string[]): Set<number>;
707
+ getAffectedLinesByDataSource(dataSourceId: string, queryKey: string[]): ReadonlySet<number>;
672
708
  /**
673
709
  * Remove a line from the dependency graph (e.g., when a line is deleted from the document).
674
710
  *
@@ -684,21 +720,33 @@ declare class DependencyGraph {
684
720
  * @param variable - The variable name
685
721
  * @returns Set of line numbers that read this variable, or empty set if none
686
722
  */
687
- getConsumers(variable: string): Set<number>;
723
+ getConsumers(variable: string): ReadonlySet<number>;
724
+ /**
725
+ * Get all line numbers that produce (write) a given key.
726
+ *
727
+ * The mirror of {@link getConsumers}, and the reason the producer index
728
+ * exists: for a category tag it is the group's membership, so an aggregate
729
+ * over `#food` costs the size of the group rather than the size of the
730
+ * document.
731
+ *
732
+ * @param key - The key, from {@link edgeKey}
733
+ * @returns Set of line numbers that write this key, or empty set if none
734
+ */
735
+ getProducers(key: string): ReadonlySet<number>;
688
736
  /**
689
737
  * Get all variables that a line depends on (reads).
690
738
  *
691
739
  * @param lineNumber - The line number to query
692
740
  * @returns Set of variable names this line reads, or empty set if none
693
741
  */
694
- getDependencies(lineNumber: number): Set<string>;
742
+ getDependencies(lineNumber: number): ReadonlySet<string>;
695
743
  /**
696
744
  * Get all variables that a line writes (assigns to).
697
745
  *
698
746
  * @param lineNumber - The line number to query
699
747
  * @returns Set of variable names this line writes, or empty set if none
700
748
  */
701
- getWrites(lineNumber: number): Set<string>;
749
+ getWrites(lineNumber: number): ReadonlySet<string>;
702
750
  /**
703
751
  * Get a serializable snapshot of the entire dependency graph for diagnostics.
704
752
  *
@@ -1,5 +1,5 @@
1
1
  import { V as Value } from './Value-B55Hvn3e.js';
2
- import { V as VM } from './VMBuiltins-BnE1KdV5.js';
2
+ import { V as VM } from './VMBuiltins-CjuRXAOD.js';
3
3
  import { U as UserFunctionDef } from './BytecodeBuilder-DSWKZi4f.js';
4
4
 
5
5
  /**
@@ -1,5 +1,5 @@
1
1
  import { V as Value } from './Value-B55Hvn3e.cjs';
2
- import { V as VM } from './VMBuiltins-RpmR0yHh.cjs';
2
+ import { V as VM } from './VMBuiltins-DR0PyI_g.cjs';
3
3
  import { U as UserFunctionDef } from './BytecodeBuilder-DSWKZi4f.cjs';
4
4
 
5
5
  /**