@rho-workspace/rho 0.5.6 → 0.5.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 (211) hide show
  1. package/dist/agent-connect.d.ts +36 -2
  2. package/dist/agent-connect.js +64 -11
  3. package/dist/answer-board.js +3 -3
  4. package/dist/bridge.js +6 -1
  5. package/dist/browser-access.d.ts +1 -0
  6. package/dist/browser-access.js +5 -3
  7. package/dist/capabilities.d.ts +21 -5
  8. package/dist/capabilities.js +6 -4
  9. package/dist/cli.js +76 -11
  10. package/dist/composition.d.ts +1 -1
  11. package/dist/composition.js +13 -3
  12. package/dist/connect-cli.d.ts +50 -0
  13. package/dist/connect-cli.js +128 -0
  14. package/dist/contract.generated.d.ts +1191 -278
  15. package/dist/contract.generated.js +1 -1
  16. package/dist/contracts/owners.generated.js +845 -47
  17. package/dist/conversation-presentation.d.ts +9 -2
  18. package/dist/conversation-presentation.js +3 -1
  19. package/dist/conversations.d.ts +154 -8
  20. package/dist/conversations.js +147 -24
  21. package/dist/discovery.js +2 -2
  22. package/dist/extension-contract.d.ts +19 -0
  23. package/dist/extension-contract.js +15 -6
  24. package/dist/extension-markets.d.ts +74 -0
  25. package/dist/extension-markets.js +166 -0
  26. package/dist/extensions.d.ts +29 -1
  27. package/dist/extensions.js +13 -4
  28. package/dist/external-work.d.ts +22 -0
  29. package/dist/external-work.js +91 -0
  30. package/dist/plugin-cli.js +75 -3
  31. package/dist/plugin-inventory.d.ts +56 -0
  32. package/dist/plugin-inventory.js +41 -0
  33. package/dist/plugin-inventory.json +1581 -102
  34. package/dist/plugin-presentation.d.ts +105 -2
  35. package/dist/plugin-presentation.js +59 -5
  36. package/dist/plugins.d.ts +14 -3
  37. package/dist/plugins.js +12 -6
  38. package/dist/project-notes.js +1 -1
  39. package/dist/protocol.d.ts +6 -0
  40. package/dist/protocol.js +30 -12
  41. package/dist/replay.js +4 -0
  42. package/dist/response-contracts.js +26 -11
  43. package/dist/running.d.ts +10 -0
  44. package/dist/running.js +45 -0
  45. package/dist/server.d.ts +2 -0
  46. package/dist/server.js +71 -8
  47. package/dist/state.d.ts +8 -0
  48. package/dist/state.js +8 -0
  49. package/dist/studio-layout.js +3 -3
  50. package/dist/task-frame.d.ts +1 -1
  51. package/dist/timeline-events.d.ts +1 -0
  52. package/dist/timeline-events.js +8 -1
  53. package/dist/ui-context.d.ts +73 -0
  54. package/dist/ui-context.js +98 -0
  55. package/dist/ui-contract.d.ts +23 -0
  56. package/dist/web/.vite/manifest.json +133 -74
  57. package/dist/web/assets/answer-board-CeYS8k8y.js +1 -0
  58. package/dist/web/assets/choices-D8vGwsl2.js +1 -0
  59. package/dist/web/assets/{console-Trqc0swD.js → console-Dyuo1q4N.js} +4 -4
  60. package/dist/web/assets/{dist-pF902jHA.js → dist-CSiCyBml.js} +1 -1
  61. package/dist/web/assets/{dist-DpIHRk3B.js → dist-D9FLb9Hm.js} +1 -1
  62. package/dist/web/assets/{dist-eMx6b8N1.js → dist-FXbTBwML.js} +1 -1
  63. package/dist/web/assets/{dist-DZmszrjG.js → dist-l3DUhbw5.js} +1 -1
  64. package/dist/web/assets/{explore-in-BCHqjJ7B.js → explore-in-BfSFGRh2.js} +1 -1
  65. package/dist/web/assets/graph-BU2_o5uA.js +2 -0
  66. package/dist/web/assets/index-C16mXVvk.css +1 -0
  67. package/dist/web/assets/index-CWg9XYXx.js +952 -0
  68. package/dist/web/assets/markdown.reader-Ct2-e2Zt.png +0 -0
  69. package/dist/web/assets/{notes-meaning-DcVGUBBY.js → notes-meaning-T2m5zQfv.js} +1 -1
  70. package/dist/web/assets/overview-PzfLJCo-.js +6 -0
  71. package/dist/web/assets/preview-BguEdsza.js +1 -0
  72. package/dist/web/assets/previews-pWNik-2g.js +3 -0
  73. package/dist/web/assets/{review-DYP0aCXb.js → review-Dy_85lcH.js} +1 -1
  74. package/dist/web/assets/rho.editor-CCJeyHma.png +0 -0
  75. package/dist/web/assets/rho.flow-WkMj6fRS.png +0 -0
  76. package/dist/web/assets/rho.media-Cc6amodO.png +0 -0
  77. package/dist/web/assets/rho.notebook-6vsZKGDq.png +0 -0
  78. package/dist/web/assets/rho.object-BGoeXjte.png +0 -0
  79. package/dist/web/assets/rho.plots-yIiISCkb.png +0 -0
  80. package/dist/web/assets/rho.scatter-DwHxesCs.png +0 -0
  81. package/dist/web/assets/rho.servers-DKf_mnNq.png +0 -0
  82. package/dist/web/assets/rho.table-DWOGaWDa.png +0 -0
  83. package/dist/web/assets/rho.variables-CKcWczhQ.png +0 -0
  84. package/dist/web/assets/{skills-1ec0Z-jx.js → skills-Dyz7mBlc.js} +1 -1
  85. package/dist/web/assets/{skills-work-DNexV4OJ.js → skills-work-BEGqlCkx.js} +1 -1
  86. package/dist/web/assets/studio-process-D-ykbC4D.js +2 -0
  87. package/dist/web/assets/timeline-events-DKTho9fU.js +1 -0
  88. package/dist/web/assets/work-turns-DSabCogR.js +1 -0
  89. package/dist/web/assets/{workspace-COmCbGV3.js → workspace-DfjBL--r.js} +2 -2
  90. package/dist/web/composition.json +138 -83
  91. package/dist/web/index.html +3 -3
  92. package/dist/web/renderer.json +1 -1
  93. package/dist/web-assets.js +2 -2
  94. package/dist/work-graph.d.ts +11 -1
  95. package/dist/work-graph.js +16 -5
  96. package/dist/workspace.d.ts +2 -2
  97. package/dist/workspace.js +8 -7
  98. package/docs/FRONTEND.md +53 -11
  99. package/docs/HEADLESS.md +104 -11
  100. package/docs/THIRD-PARTY-PLUGINS.md +75 -3
  101. package/node_modules/@rho-workspace/plugin-agent/README.md +5 -0
  102. package/node_modules/@rho-workspace/plugin-agent/dist/acp-command.d.ts +31 -0
  103. package/node_modules/@rho-workspace/plugin-agent/dist/acp-command.js +45 -10
  104. package/node_modules/@rho-workspace/plugin-agent/dist/acp-conversation.js +19 -3
  105. package/node_modules/@rho-workspace/plugin-agent/dist/external-session-contract.js +3 -2
  106. package/node_modules/@rho-workspace/plugin-agent/dist/external-session.d.ts +1 -1
  107. package/node_modules/@rho-workspace/plugin-agent/dist/index.d.ts +8 -0
  108. package/node_modules/@rho-workspace/plugin-agent/dist/index.js +3 -2
  109. package/node_modules/@rho-workspace/plugin-agent/dist/mcp-entry.d.ts +13 -0
  110. package/node_modules/@rho-workspace/plugin-agent/dist/mcp-entry.js +13 -0
  111. package/node_modules/@rho-workspace/plugin-agent/dist/mcp.js +16 -8
  112. package/node_modules/@rho-workspace/plugin-agent/dist/native-sessions.d.ts +1 -1
  113. package/node_modules/@rho-workspace/plugin-agent/package.json +1 -1
  114. package/node_modules/@rho-workspace/plugin-figures/LICENSE +661 -0
  115. package/node_modules/@rho-workspace/plugin-figures/README.md +191 -0
  116. package/node_modules/@rho-workspace/plugin-figures/dist/index.d.ts +236 -0
  117. package/node_modules/@rho-workspace/plugin-figures/dist/index.js +165 -0
  118. package/node_modules/@rho-workspace/plugin-figures/dist/operations.d.ts +44 -0
  119. package/node_modules/@rho-workspace/plugin-figures/dist/operations.js +39 -0
  120. package/node_modules/@rho-workspace/plugin-figures/dist/presentation.d.ts +19 -0
  121. package/node_modules/@rho-workspace/plugin-figures/dist/presentation.js +26 -0
  122. package/node_modules/@rho-workspace/plugin-figures/dist/record.d.ts +188 -0
  123. package/node_modules/@rho-workspace/plugin-figures/dist/record.js +143 -0
  124. package/node_modules/@rho-workspace/plugin-figures/dist/ui-styles.d.ts +1 -0
  125. package/node_modules/@rho-workspace/plugin-figures/dist/ui-styles.js +68 -0
  126. package/node_modules/@rho-workspace/plugin-figures/dist/ui.d.ts +56 -0
  127. package/node_modules/@rho-workspace/plugin-figures/dist/ui.js +363 -0
  128. package/node_modules/@rho-workspace/plugin-figures/package.json +41 -0
  129. package/node_modules/@rho-workspace/plugin-figures/py/figures.py +856 -0
  130. package/node_modules/@rho-workspace/plugin-figures/r/figures.R +775 -0
  131. package/node_modules/@rho-workspace/plugin-kernel/README.md +27 -3
  132. package/node_modules/@rho-workspace/plugin-kernel/dist/contract.d.ts +12 -0
  133. package/node_modules/@rho-workspace/plugin-kernel/dist/contract.js +8 -1
  134. package/node_modules/@rho-workspace/plugin-kernel/dist/index.d.ts +61 -0
  135. package/node_modules/@rho-workspace/plugin-kernel/dist/index.js +125 -9
  136. package/node_modules/@rho-workspace/plugin-kernel/dist/session-scope.d.ts +14 -0
  137. package/node_modules/@rho-workspace/plugin-kernel/dist/session-scope.js +9 -0
  138. package/node_modules/@rho-workspace/plugin-kernel/dist/ui-base.d.ts +23 -5
  139. package/node_modules/@rho-workspace/plugin-kernel/dist/ui-notebook-work.js +1 -1
  140. package/node_modules/@rho-workspace/plugin-kernel/dist/ui-notebook.d.ts +6 -4
  141. package/node_modules/@rho-workspace/plugin-kernel/dist/ui-notebook.js +44 -17
  142. package/node_modules/@rho-workspace/plugin-kernel/dist/ui-styles.js +6 -0
  143. package/node_modules/@rho-workspace/plugin-kernel/package.json +1 -1
  144. package/node_modules/@rho-workspace/plugin-kernel/runners/runner.R +144 -4
  145. package/node_modules/@rho-workspace/plugin-kernel/runners/runner.py +106 -1
  146. package/node_modules/@rho-workspace/plugin-tables/README.md +5 -0
  147. package/node_modules/@rho-workspace/plugin-tables/dist/index.d.ts +1 -0
  148. package/node_modules/@rho-workspace/plugin-tables/dist/index.js +19 -3
  149. package/node_modules/@rho-workspace/plugin-tables/dist/ui.js +1 -1
  150. package/node_modules/@rho-workspace/plugin-tables/package.json +1 -1
  151. package/node_modules/jose/dist/webapi/jwe/general/decrypt.js +3 -2
  152. package/node_modules/jose/dist/webapi/jwks/remote.js +1 -1
  153. package/node_modules/jose/dist/webapi/jws/general/verify.js +2 -8
  154. package/node_modules/jose/dist/webapi/lib/jwe_decrypt.js +11 -8
  155. package/node_modules/jose/dist/webapi/lib/key_management.js +0 -1
  156. package/node_modules/jose/package.json +1 -1
  157. package/package.json +6 -4
  158. package/protocol/index.json +91 -55
  159. package/protocol/operations/agent.sessions.list.json +13 -5
  160. package/protocol/operations/agent.sessions.read.json +13 -5
  161. package/protocol/operations/capability.list.json +382 -6
  162. package/protocol/operations/composition.apply.json +10 -2
  163. package/protocol/operations/composition.delete.json +845 -0
  164. package/protocol/operations/composition.read.json +10 -2
  165. package/protocol/operations/composition.undo.json +10 -2
  166. package/protocol/operations/conversation.agents.add.json +346 -0
  167. package/protocol/operations/conversation.agents.remove.json +340 -0
  168. package/protocol/operations/conversation.decide.json +196 -0
  169. package/protocol/operations/conversation.permission.json +196 -0
  170. package/protocol/operations/conversation.read.json +196 -0
  171. package/protocol/operations/conversation.send.json +265 -0
  172. package/protocol/operations/conversation.settings.check.json +213 -495
  173. package/protocol/operations/conversation.settings.read.json +211 -490
  174. package/protocol/operations/conversation.settings.update.json +235 -554
  175. package/protocol/operations/conversation.start.json +267 -5
  176. package/protocol/operations/conversation.stop.json +196 -0
  177. package/protocol/operations/extension.describe.json +39 -0
  178. package/protocol/operations/extension.install.json +62 -0
  179. package/protocol/operations/extension.list.json +164 -1
  180. package/protocol/operations/extension.market.change.json +210 -0
  181. package/protocol/operations/figure.read.json +265 -0
  182. package/protocol/operations/figure.select.json +157 -0
  183. package/protocol/operations/kernel.interrupt.json +119 -0
  184. package/protocol/operations/kernel.read.json +119 -0
  185. package/protocol/operations/kernel.run.json +119 -0
  186. package/protocol/operations/observation.snapshot.json +416 -8
  187. package/protocol/operations/project.openFile.json +10 -2
  188. package/protocol/operations/session.grant.json +4 -0
  189. package/protocol/operations/shared.json +1 -0
  190. package/protocol/operations/view.close.json +10 -2
  191. package/protocol/operations/view.protection.set.json +10 -2
  192. package/vendor/composition.json +177 -112
  193. package/vendor/rho-workspace-plugin-agent-0.4.33.tgz +0 -0
  194. package/vendor/rho-workspace-plugin-figures-0.5.1.tgz +0 -0
  195. package/vendor/rho-workspace-plugin-kernel-0.5.13.tgz +0 -0
  196. package/vendor/rho-workspace-plugin-tables-0.5.2.tgz +0 -0
  197. package/web/theme.css +5 -0
  198. package/dist/web/assets/answer-board-CBkGbSLD.js +0 -1
  199. package/dist/web/assets/canvas-D8rDdfDz.js +0 -1
  200. package/dist/web/assets/choices-D-YfnFP6.js +0 -1
  201. package/dist/web/assets/graph-lENgfQNH.js +0 -2
  202. package/dist/web/assets/index-B3_OVy91.css +0 -1
  203. package/dist/web/assets/index-BpL_Kl_s.js +0 -892
  204. package/dist/web/assets/overview-CDnVKlOt.js +0 -6
  205. package/dist/web/assets/preview-BEC1E9MK.js +0 -1
  206. package/dist/web/assets/studio-process-EYQgikxq.js +0 -2
  207. package/dist/web/assets/timeline-events-Chj2Gbx7.js +0 -1
  208. package/dist/web/assets/work-turns-CT9RVcWY.js +0 -1
  209. package/vendor/rho-workspace-plugin-agent-0.4.30.tgz +0 -0
  210. package/vendor/rho-workspace-plugin-kernel-0.5.5.tgz +0 -0
  211. package/vendor/rho-workspace-plugin-tables-0.5.0.tgz +0 -0
@@ -0,0 +1,191 @@
1
+ # Figures
2
+
3
+ `@rho-workspace/plugin-figures` 0.4.0 keeps the plots a project's live R or Python session
4
+ draws as figures whose marks know the rows they were drawn from. It is built into Rho and
5
+ needs the Notebook (`kernel`).
6
+
7
+ ## What it does
8
+
9
+ The Notebook's host hands `r/figures.R` to each local R session it starts. When a cell ends,
10
+ the session draws each ggplot page again, off screen, and writes a figure record
11
+ (`rho.figure/1`) next to the plot's PNG. Every ggplot is described the same way, whatever
12
+ built it: ggplot2 itself and packages built on it (ggtree, enrichplot, Seurat, patchwork
13
+ compositions, ggrepel labels). Nothing in the describer knows a package or a domain.
14
+
15
+ - Each layer's geom is cloned for the drawing, so the plot's own objects are not changed.
16
+ The clone numbers the rows ggplot2 built (`handle_na`) and tags each panel's grob with
17
+ the plot, layer, panel and rows it drew (`draw_layer`). The picture is rendered as SVG
18
+ through svglite, and each drawn element is given `data-rho="unit.element"`.
19
+ - A drawn element stands for one built row (a point, a bar, a text label, a segment) or
20
+ for a group (a line, a polygon, a box). A label drawn in several parts (a ggrepel label
21
+ and its halo) is bound by its text. Parts that cannot be matched are reported in the
22
+ layer's `unbound` reasons; they are never guessed.
23
+ - An extra aesthetic carries each data row's number through the layer's statistic. Where
24
+ the statistic keeps its input rows (identity, jitter, most position adjustments), a mark
25
+ links to its row of the plot's data; where it computes new rows (counts, boxes,
26
+ densities) the mark keeps its computed values and no row. The source's key is its row
27
+ names, else a unique text column, else a numeric `id` or `node` column.
28
+ - Positions are read back through the scale's inverse, so a box on `scale_y_log10()` reads
29
+ in counts. The statistic still ran on the transformed values: the median drawn between
30
+ 1100 and 1200 on a log scale is their geometric mean, and that is what is recorded.
31
+ - The session is left as the cell left it: the random number state is restored, and the
32
+ describer's own output, messages and warnings are discarded.
33
+
34
+ A ggplot saved to a project file with `ggsave()` in a Notebook cell is kept as a figure of
35
+ that file. When the page starts on ggsave's device, `saved_page()` names the plot, ggsave's
36
+ file and the device's size; when the cell ends the page is described at that size and the
37
+ record is kept with the cell's plots. The cell's `saved` list names the file with its bytes
38
+ and SHA-256. A page that shows the file names it by those bytes, so the figure holds only
39
+ while the file keeps them: once anything writes the file again it is a picture, until a
40
+ cell saves it as a figure again. A file outside the project is not kept, nor one a session on
41
+ a server saved.
42
+
43
+ A page drawn on a file the code opened itself (`png()`, `jpeg()`, `tiff()`, `bmp()`, `svg()`,
44
+ `pdf()`, `cairo_pdf()`) and closed with `dev.off()` is kept the same way: DESeq2's `plotMA`,
45
+ a ComplexHeatmap's `draw()` or a ggplot's `print()` written to `results/ma.png`. The Notebook's
46
+ runner names the file and records the page as it is drawn; the extension describes the
47
+ recorded page (base graphics, a heatmap) or the plot `page_object()` named (a ggplot) at the
48
+ device's size. A file name numbered with `%d` is a figure for each page; a PDF of several pages
49
+ is not described and says so; any other file keeps its last page. A device left open in one
50
+ cell keeps drawing in the next, as in the R console, and its file is described in the cell
51
+ that closes it. A page no describer knows (a lattice page) stays a picture without a problem.
52
+
53
+ A page drawn with base graphics (`plot`, `points`, `text`, `barplot`, `abline`, `image` and
54
+ the functions built on them) is described from its recording. gridGraphics makes the page
55
+ into grobs named after what drew them, and each drawn part in a plot's window is a layer:
56
+ points, text, rect, lines, polygon, segments, circles or a reference line. Each element is a
57
+ mark that keeps the values it was drawn at, in the data's units (a log axis is read back).
58
+ A bar is named by the axis label at its middle. Axes, boxes and titles are drawn and not
59
+ bound. Points, labels and lines whose x and y are two columns of a table in the session (a
60
+ data frame, a Bioconductor `DataFrame`, a numeric matrix or a named vector) are bound to its
61
+ rows, value for value and in the table's order. Rows that were not drawn are skipped. The
62
+ layer's `mapping` names the two columns, and its `notes` count marks drawn at another
63
+ value, such as points clipped to the axis limit. A position drawn through `-log10`, `log10`,
64
+ `log2`, `log2(x + 1)` or `log1p` of a column is matched through it, and the mapping says so:
65
+ `plot(res$log2FoldChange, -log10(res$padj))` is bound as `y: -log10(padj)`. Columns drawn as
66
+ they are come first, and a value that is not finite (`-log10(0)`) matches only itself. When an
67
+ earlier layer of the same plot draws the same column on the same axis through another
68
+ transform, the later layer's note says so: points added at `log2(res$baseMean)` over
69
+ `plotMA`, whose x axis draws `baseMean` on a log scale, are placed on another scale than the
70
+ plot's own points. A part drawn from other computed values matches no table and keeps only its
71
+ drawn values.
72
+
73
+ A ComplexHeatmap is described from the heatmap list it laid out. `draw()` starts the page
74
+ before it lays the heatmaps out, so the describer keeps the frame of its method when the
75
+ page starts and reads the laid-out list when the cell ends: the rows and columns in the order
76
+ and slices drawn, and the matrix. The page is drawn again from its recording, and each cell
77
+ rectangle of a heatmap body is a mark naming its row, column, value and slices, with the
78
+ matrix row as its source (named after the session's matrix when one is identical). Cells
79
+ drawn as one image (`use_raster`) are not bound; annotations, dendrograms and names are
80
+ drawn and not bound.
81
+
82
+ A matplotlib figure in a live Python session (and what draws through matplotlib: seaborn,
83
+ pandas `.plot`) is described by `py/figures.py`, which the Notebook's host hands to each local
84
+ Python session. matplotlib keeps what each Axes drew as artists, so nothing is read from
85
+ pixels: a scatter's points are a collection with their offsets, a line keeps its vertices,
86
+ each bar is a rectangle, a heatmap (`pcolormesh`, seaborn's `heatmap` and `clustermap`) a mesh
87
+ of cells, a label its text and position. Each artist is given an id for the drawing, the
88
+ figure is written as SVG at its own size, and the elements matplotlib wrote for each artist
89
+ are named after its marks; when they cannot be matched one for one, the layer says why. An
90
+ artist drawn `rasterized=True` (a large scatter, to keep its file small) is drawn as vectors
91
+ for the figure; the figure and its picture are left as the code made them.
92
+
93
+ - Points, line vertices and labels keep the values they were drawn at, in the data's units
94
+ (a log axis reads in counts). Where their x and y are two columns of a table in the session
95
+ (a DataFrame, a numeric Series with its index, a 2-D numeric array), they are bound to its
96
+ rows in the table's order, skipping rows not drawn. A position drawn through `-log10`,
97
+ `log10`, `log2`, `log2(x + 1)` or `log1p` of a column is matched through it, and the
98
+ layer's mapping says so (`y: -log10(padj)`). When an earlier layer of the same Axes draws
99
+ the same column on the same axis through another transform, the later layer's note says so,
100
+ as in R: points added at `np.log2(sig.baseMean)` over an MA plot whose x axis draws
101
+ `baseMean` on a log scale. Marks all drawn at one value of the other axis (genes not
102
+ tested, drawn at 0) are bound by their own axis alone, and noted. Tables the cell's code
103
+ assigns, then those it reads, are compared first. A DataFrame is keyed by its
104
+ text index (named after the index's name), else a unique text column.
105
+ - A label (`ax.text`, `ax.annotate`) drawn at a bound point's position names that point's row,
106
+ whatever order the labels were drawn in; otherwise one whose text names rows of a table
107
+ another layer of the plot is bound to is bound by its text, wherever it was moved to. A text
108
+ placed in the axes' own coordinates (a note in a corner) is a layer of its own.
109
+ - A heatmap cell names its row, column and value. When a table of the session holds the same
110
+ values, also with rows and columns reordered (a clustered heatmap), the cell's source row
111
+ is that table's row, and tick labels that name other rows of it than the values they sit
112
+ beside are noted (*29 of 30 rows are labelled with another row of heat_z: row 1 is labelled
113
+ SPARCL1 and draws the values of PDPN*). Otherwise the drawn matrix is the source, named by
114
+ its tick labels; labels set on the cells' edges, one for each, are read in their order and
115
+ noted.
116
+ - A bar is named by the category drawn at its middle; positions on an axis of categories are
117
+ named by their category. Horizontal and vertical reference lines (`axhline`, `axvline`)
118
+ keep their intercept. Segments and polygons keep their ends and extents.
119
+ - The legend and colour bar are the plot's scales; a seaborn facet's title (`dex = trt`) is
120
+ its facet.
121
+ - What the describer notices (marks drawn elsewhere than their values, a column drawn on
122
+ another scale than an earlier layer draws it, labels naming other rows) is kept as the
123
+ layer's `notes`, carried in the cell's plot output for the Agent and shown under the figure.
124
+
125
+ A figure saved with `savefig()` to a file in the project is kept as a figure of that file,
126
+ like a ggplot saved with `ggsave()`: the Notebook's runner watches `Figure.savefig`, the figure
127
+ is described as it was saved (with its `bbox_inches`), and the cell's `saved` list names the
128
+ file with its bytes when the cell ends, also when the cell closed the figure.
129
+
130
+ Other grid drawings and images drawn with `imshow` stay pictures. A plot output nothing
131
+ described has neither `figure` nor `figureProblem`: nothing failed, nothing described them.
132
+
133
+ ## Who uses it
134
+
135
+ - **The person** points at a mark to read its row, clicks or drags to select marks in the
136
+ Notebook and in the Plots view, reads the selected rows in a table, and asks the Agent
137
+ about them. The selection is kept and travels with the next message.
138
+ - **The Agent** sees each plot output's summary (layers, marks, how many are bound) in
139
+ `kernel.run` and `kernel.read`. It reads the figure with `figure.read`, finds marks by
140
+ condition (`where`), reads the rows of the person's selection, and points back with
141
+ `figure.select`. Its marks are shown highlighted, with its note, on the original plot.
142
+
143
+ ## Operations
144
+
145
+ A figure is named by `{cellId, plot}`, a plot a cell drew, or by a file a cell saved it as:
146
+ `{path}` reads the file as it is now, `{sha256}` names exact bytes (a page passes the
147
+ version it shows).
148
+
149
+ `figure.read {cellId, plot | path | sha256, form?, marks?, selection?, where?, limit?}` returns the summary
150
+ of a figure (or, with `form: 'view'`, the whole record for drawing it), the selections kept
151
+ for it, and rows for marks named directly, by a kept selection or by a condition.
152
+
153
+ `figure.select {cellId, plot | path | sha256, marks, note?, changeId}` keeps a selection. It is
154
+ immutable and named by the figure, the principal and its `changeId`; a saved file's selections
155
+ follow its bytes: the same request returns the same
156
+ selection, and the same `changeId` with other marks is refused.
157
+
158
+ ## Limits
159
+
160
+ The protected resource is the session's time and the project's state folder.
161
+
162
+ | Limit | Value | When it is exceeded |
163
+ | --- | --- | --- |
164
+ | Marks in a figure (`maxMarks`) | 50 000 | The plot stays a picture; `figureProblem` says why. |
165
+ | Time to describe (`maxSeconds`) | 20 s | Describing stops; the plot stays a picture. |
166
+ | Record size (`maxBytes`, kernel `maxFigureBytes`) | 16 MiB | The record is not kept; `figureProblem` says how large it was. |
167
+ | Columns kept for a source or a layer (`maxColumns`) | 40 | Later columns are left out of the record. |
168
+ | Session tables compared with a base graphics or matplotlib layer (`maxTables`) | 64 | Later tables are not compared; the layer keeps its drawn values. |
169
+ | Numeric columns of one table compared with a layer (`maxComparedColumns`) | 64 | Later columns are not compared. A table with fewer rows than the layer's marks is not compared. |
170
+ | Cells of one compared table (`maxTableCells`) | 5 000 000 | A larger table is not compared. |
171
+ | Characters of a text value (`maxCellChars`) | 160 | The value is cut and ends with `…`. |
172
+ | Marks in a selection | 5 000 | The selection is refused. |
173
+ | Saved files described per cell (kernel `maxPlots`) | 8 | Later files stay pictures. |
174
+ | Selections kept per figure | 32 | The oldest are removed. |
175
+ | Rows in one read | 500 (50 by default) | The rest are counted in `rowsTotal`. |
176
+
177
+ ## Check
178
+
179
+ ```sh
180
+ npm run test:figures
181
+ ```
182
+
183
+ It describes real ggplot2 and patchwork plots, base graphics pages and a ComplexHeatmap in
184
+ R, and matplotlib and seaborn figures in Python, through the Notebook and reads, matches and
185
+ selects their marks. It saves plots with
186
+ `ggsave()` as PNG and PDF, and writes base graphics, ggplot and heatmap pages to files the
187
+ cell opens with `png()`, `pdf()` and `svg()`, and reads them by path and by bytes until the
188
+ file is written again. Without R and ggplot2, svglite, xml2 and jsonlite the checks are skipped, not passed;
189
+ so are the base graphics check without gridGraphics and the heatmap check without
190
+ ComplexHeatmap. The Python checks run `RHO_PYTHON` (else `python3`) and are skipped without
191
+ matplotlib, numpy and pandas; the seaborn check without seaborn.
@@ -0,0 +1,236 @@
1
+ import { type Where } from './record.js';
2
+ export { parseRecord, figureSummary, markRow, matchMarks, markName, parseMark, elementMarks } from './record.js';
3
+ export type { FigureRecord, FigureLayer, FigureSource, MarkRef, MarkRow, Where } from './record.js';
4
+ export declare const rExtension: string;
5
+ export declare const pythonExtension: string;
6
+ export interface FigureCell {
7
+ cellId: string;
8
+ number: number;
9
+ by: string;
10
+ purpose: string | null;
11
+ code: string;
12
+ language: string;
13
+ endedAt: string | null;
14
+ sessionId: string;
15
+ }
16
+ export interface FigureSourcePort {
17
+ record(cellId: string, plot: number): Promise<{
18
+ cell: FigureCell;
19
+ output: unknown;
20
+ bytes: Uint8Array;
21
+ }>;
22
+ saved?(sha256: string): Promise<{
23
+ cell: FigureCell;
24
+ output: unknown;
25
+ bytes: Uint8Array;
26
+ }>;
27
+ }
28
+ export type FigureRef = {
29
+ cellId?: string;
30
+ plot?: number;
31
+ path?: string;
32
+ sha256?: string;
33
+ };
34
+ export declare const figureLimits: {
35
+ maxSelectionMarks: number;
36
+ maxNoteChars: number;
37
+ keptSelections: number;
38
+ maxRows: number;
39
+ defaultRows: number;
40
+ matchMarks: number;
41
+ };
42
+ export type FigureLimits = typeof figureLimits;
43
+ export interface Selection {
44
+ selectionId: string;
45
+ cellId: string;
46
+ plot?: number;
47
+ path?: string;
48
+ sha256?: string;
49
+ digest: string;
50
+ by: string;
51
+ at: string;
52
+ changeId: string;
53
+ note: string;
54
+ count: number;
55
+ layers: Array<{
56
+ layer: number;
57
+ geom: string;
58
+ marks: number;
59
+ }>;
60
+ marks: string[];
61
+ }
62
+ export declare class Figures {
63
+ private root;
64
+ private port;
65
+ private limits;
66
+ constructor(root: string, port: FigureSourcePort, limits?: Partial<FigureLimits>);
67
+ private folder;
68
+ private resolve;
69
+ private selections;
70
+ read(input: FigureRef & {
71
+ form?: 'summary' | 'view' | 'selections';
72
+ marks?: string[];
73
+ selection?: string;
74
+ where?: Where[];
75
+ limit?: number;
76
+ }): Promise<Record<string, unknown>>;
77
+ select(input: FigureRef & {
78
+ marks: string[];
79
+ note?: string;
80
+ changeId: string;
81
+ }, by: string): Promise<{
82
+ selection: Selection;
83
+ }>;
84
+ brief(ref: FigureRef, selectionId: string, rows: number): Promise<{
85
+ cell: number;
86
+ purpose: string | null;
87
+ selectionId: string;
88
+ count: number;
89
+ note: string;
90
+ layers: {
91
+ layer: number;
92
+ geom: string;
93
+ marks: number;
94
+ }[];
95
+ rows: ({
96
+ values: Record<string, import("./record.js").Cell>;
97
+ facets?: Record<string, string> | undefined;
98
+ mark: string;
99
+ geom: string;
100
+ } | {
101
+ built: Record<string, import("./record.js").Cell>;
102
+ facets?: Record<string, string> | undefined;
103
+ mark: string;
104
+ geom: string;
105
+ } | null)[];
106
+ plot: number;
107
+ cellId: string;
108
+ } | {
109
+ cell: number;
110
+ purpose: string | null;
111
+ selectionId: string;
112
+ count: number;
113
+ note: string;
114
+ layers: {
115
+ layer: number;
116
+ geom: string;
117
+ marks: number;
118
+ }[];
119
+ rows: ({
120
+ values: Record<string, import("./record.js").Cell>;
121
+ facets?: Record<string, string> | undefined;
122
+ mark: string;
123
+ geom: string;
124
+ } | {
125
+ built: Record<string, import("./record.js").Cell>;
126
+ facets?: Record<string, string> | undefined;
127
+ mark: string;
128
+ geom: string;
129
+ } | null)[];
130
+ path: string | undefined;
131
+ sha256: string | undefined;
132
+ cellId: string;
133
+ }>;
134
+ }
135
+ export declare const plugin: {
136
+ entryVersion: "1.0";
137
+ id: string;
138
+ package: string;
139
+ version: string;
140
+ requires: string[];
141
+ capabilities: string[];
142
+ presentation: import("@rho-workspace/plugin-contracts").PluginPresentation;
143
+ operations: ({
144
+ requires: string[];
145
+ inputSchema: {
146
+ type: string;
147
+ additionalProperties: boolean;
148
+ properties: Record<string, unknown>;
149
+ required: string[];
150
+ };
151
+ outputSchema: {
152
+ type: string;
153
+ additionalProperties: boolean;
154
+ properties: Record<string, unknown>;
155
+ required: string[];
156
+ };
157
+ effect: "read";
158
+ method: string;
159
+ presentation: {
160
+ order: number;
161
+ displayName: string;
162
+ description: string;
163
+ readOnly: true;
164
+ runsCode: false;
165
+ } | {
166
+ order: number;
167
+ displayName: string;
168
+ description: string;
169
+ readOnly: false;
170
+ runsCode: false;
171
+ };
172
+ version: "2.0";
173
+ } | {
174
+ requires: string[];
175
+ inputSchema: {
176
+ type: string;
177
+ additionalProperties: boolean;
178
+ properties: Record<string, unknown>;
179
+ required: string[];
180
+ };
181
+ outputSchema: {
182
+ type: string;
183
+ additionalProperties: boolean;
184
+ properties: Record<string, unknown>;
185
+ required: string[];
186
+ };
187
+ effect: "metadata";
188
+ method: string;
189
+ presentation: {
190
+ order: number;
191
+ displayName: string;
192
+ description: string;
193
+ readOnly: true;
194
+ runsCode: false;
195
+ } | {
196
+ order: number;
197
+ displayName: string;
198
+ description: string;
199
+ readOnly: false;
200
+ runsCode: false;
201
+ };
202
+ version: "2.0";
203
+ })[];
204
+ skills: {
205
+ id: string;
206
+ version: string;
207
+ content: string;
208
+ digest: string;
209
+ presentation: import("@rho-workspace/plugin-contracts").EntryPresentation;
210
+ intent: string;
211
+ requires: string[];
212
+ example: {
213
+ method: string;
214
+ input: {
215
+ cellId: string;
216
+ plot: number;
217
+ where: {
218
+ column: string;
219
+ op: string;
220
+ value: number;
221
+ }[];
222
+ limit: number;
223
+ };
224
+ };
225
+ failureRecipes: {
226
+ kind: string;
227
+ message: string;
228
+ next: string;
229
+ }[];
230
+ verify: {
231
+ command: string;
232
+ description: string;
233
+ };
234
+ }[];
235
+ components: unknown[];
236
+ };
@@ -0,0 +1,165 @@
1
+ import * as fs from 'node:fs/promises';
2
+ import { createReadStream } from 'node:fs';
3
+ import { createHash } from 'node:crypto';
4
+ import path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { atomicJson, confined, digest, now, readJson, requireThat, stateDirectory } from '@rho-workspace/plugin-contracts';
7
+ import { operationContracts } from './operations.js';
8
+ import { operationPresentation, presentation, skillPresentation } from './presentation.js';
9
+ import { figureSummary, markName, markRow, matchMarks, parseMark, parseRecord } from './record.js';
10
+ export { parseRecord, figureSummary, markRow, matchMarks, markName, parseMark, elementMarks } from './record.js';
11
+ // The R file a live R session sources to describe what its plots draw (see r/figures.R), and the
12
+ // Python file a live Python session runs to describe its matplotlib figures (py/figures.py). The
13
+ // Notebook's host hands them to each local session it starts.
14
+ export const rExtension = fileURLToPath(new URL('../r/figures.R', import.meta.url));
15
+ export const pythonExtension = fileURLToPath(new URL('../py/figures.py', import.meta.url));
16
+ const sha256Pattern = /^[a-f0-9]{64}$/;
17
+ async function fileDigest(file) {
18
+ const hash = createHash('sha256');
19
+ for await (const chunk of createReadStream(file))
20
+ hash.update(chunk);
21
+ return hash.digest('hex');
22
+ }
23
+ // The protected resource is the project's state folder. A selection keeps at most
24
+ // maxSelectionMarks marks and a note of maxNoteChars; the newest keptSelections of each figure
25
+ // are kept and older ones are removed. A read returns at most maxRows rows (defaultRows unless
26
+ // asked) and names at most matchMarks of the marks a condition matched, saying how many there
27
+ // were in all.
28
+ export const figureLimits = { maxSelectionMarks: 5000, maxNoteChars: 500, keptSelections: 32, maxRows: 500, defaultRows: 50, matchMarks: 5000 };
29
+ export class Figures {
30
+ root;
31
+ port;
32
+ limits;
33
+ constructor(root, port, limits = {}) {
34
+ this.root = root;
35
+ this.port = port;
36
+ this.limits = { ...figureLimits, ...limits };
37
+ }
38
+ folder(key) { return stateDirectory(this.root, `.rho/plugins/figures/${key}`); }
39
+ // The record a reference names, with the name its selections are kept under.
40
+ async resolve(ref) {
41
+ if (ref.cellId !== undefined || ref.plot !== undefined) {
42
+ requireThat(typeof ref.cellId === 'string' && Number.isInteger(ref.plot) && ref.path === undefined && ref.sha256 === undefined, 'invalid', 'Name a figure by cellId and plot, or by path or sha256 of a saved file.');
43
+ const found = await this.port.record(ref.cellId, ref.plot);
44
+ return { ...found, id: { cellId: ref.cellId, plot: ref.plot }, key: `${ref.cellId}-${ref.plot}`, parts: [ref.cellId, String(ref.plot)] };
45
+ }
46
+ requireThat(ref.path !== undefined || ref.sha256 !== undefined, 'invalid', 'Name a figure by cellId and plot, or by path or sha256 of a saved file.');
47
+ requireThat(this.port.saved, 'not_found', 'Figures saved to files are not kept here.');
48
+ let sha256 = ref.sha256;
49
+ if (sha256 === undefined) {
50
+ const file = await confined(this.root, ref.path);
51
+ const stat = await fs.stat(file).catch(() => null);
52
+ requireThat(stat?.isFile(), 'not_found', `${ref.path} is not a file in the project.`);
53
+ sha256 = await fileDigest(file);
54
+ }
55
+ requireThat(sha256Pattern.test(sha256), 'invalid', 'sha256 is the hex SHA-256 of the file\'s bytes.');
56
+ const found = await this.port.saved(sha256);
57
+ const saved = found.output;
58
+ return { ...found, id: { path: ref.path ?? saved.path ?? '', sha256, cellId: found.cell.cellId }, key: `file-${sha256.slice(0, 32)}`, parts: ['file', sha256] };
59
+ }
60
+ async selections(key) {
61
+ const folder = await this.folder(key);
62
+ const names = (await fs.readdir(folder)).filter(n => /^f-[a-f0-9]{16}\.json$/.test(n));
63
+ const all = await Promise.all(names.map(n => readJson(path.join(folder, n)).catch(() => null)));
64
+ return all.filter((s) => !!s).sort((a, b) => b.at.localeCompare(a.at));
65
+ }
66
+ async read(input) {
67
+ const { cell, output, bytes, id, key } = await this.resolve(input);
68
+ const record = parseRecord(bytes), figureDigest = digest(bytes);
69
+ const selections = await this.selections(key);
70
+ const view = input.form === 'view';
71
+ if (input.form === 'selections')
72
+ return { figure: { ...id, digest: figureDigest }, selections };
73
+ const result = {
74
+ figure: { ...id, digest: figureDigest, cell: { number: cell.number, by: cell.by, purpose: cell.purpose, language: cell.language, endedAt: cell.endedAt, code: view ? undefined : cell.code }, output },
75
+ selections: selections.map(s => view ? s : { ...s, marks: undefined }),
76
+ ...(view ? { record } : { summary: figureSummary(record) }),
77
+ };
78
+ let refs = null;
79
+ if (input.selection) {
80
+ const chosen = selections.find(s => s.selectionId === input.selection);
81
+ requireThat(chosen, 'not_found', 'This figure keeps no such selection; it may have been one of the oldest and removed.');
82
+ refs = chosen.marks.map(m => parseMark(m)).filter(Boolean);
83
+ }
84
+ if (input.marks) {
85
+ const named = input.marks.map(m => parseMark(m));
86
+ requireThat(named.every(r => r && r.layer < record.figure.layers.length && r.mark < record.figure.layers[r.layer].marks), 'invalid', 'A mark names no layer or row of this figure. Marks are "layer.row", both from 0, as figure.read names them.');
87
+ refs = named;
88
+ }
89
+ if (input.where) {
90
+ const known = new Set([...record.sources.flatMap(s => s.columns.map(c => c.name)), ...record.marks.flatMap(m => m.columns.map(c => c.name))]);
91
+ for (const w of input.where)
92
+ requireThat(known.has(w.column), 'invalid', `No source or built column is named ${w.column}. The summary lists each source's columns and each layer's built columns.`);
93
+ const matched = matchMarks(record, input.where, this.limits.matchMarks);
94
+ result.matched = { total: matched.total, marks: matched.marks.map(markName), cut: matched.total > matched.marks.length };
95
+ if (!refs)
96
+ refs = matched.marks;
97
+ }
98
+ if (refs) {
99
+ const limit = Math.min(input.limit ?? this.limits.defaultRows, this.limits.maxRows);
100
+ result.rowsTotal = refs.length;
101
+ result.rows = refs.slice(0, limit).map(r => markRow(record, r)).filter(Boolean);
102
+ }
103
+ return result;
104
+ }
105
+ // A selection never changes once kept. The same changeId from the same principal names the
106
+ // same selection; with other marks it is refused.
107
+ async select(input, by) {
108
+ const { bytes, id, key, parts } = await this.resolve(input);
109
+ const record = parseRecord(bytes), figureDigest = digest(bytes);
110
+ const marks = [...new Set(input.marks)];
111
+ requireThat(marks.length <= this.limits.maxSelectionMarks, 'limit', `A selection keeps at most ${this.limits.maxSelectionMarks} marks.`, { limit: 'maxSelectionMarks', maximum: this.limits.maxSelectionMarks });
112
+ const refs = marks.map(m => parseMark(m));
113
+ requireThat(refs.every(r => r && r.layer < record.figure.layers.length && r.mark < record.figure.layers[r.layer].marks), 'invalid', 'A mark names no layer or row of this figure. Marks are "layer.row", both from 0, as figure.read names them.');
114
+ const note = (input.note ?? '').trim();
115
+ requireThat(note.length <= this.limits.maxNoteChars, 'limit', `A note has at most ${this.limits.maxNoteChars} characters.`);
116
+ const selectionId = 'f-' + digest([...parts, by, input.changeId].join('\u0000')).slice(0, 16);
117
+ const folder = await this.folder(key), file = path.join(folder, selectionId + '.json');
118
+ const known = await readJson(file, null);
119
+ if (known) {
120
+ requireThat(known.marks.length === marks.length && known.marks.every((m, i) => m === marks[i]) && known.note === note, 'conflict', 'This change identity already names a different selection.');
121
+ return { selection: known };
122
+ }
123
+ const byLayer = new Map();
124
+ for (const r of refs)
125
+ byLayer.set(r.layer, (byLayer.get(r.layer) ?? 0) + 1);
126
+ const selection = { selectionId, ...id, digest: figureDigest, by, at: now(), changeId: input.changeId, note, count: marks.length,
127
+ layers: [...byLayer].map(([layer, n]) => ({ layer, geom: record.figure.layers[layer].geom, marks: n })), marks };
128
+ await atomicJson(file, selection);
129
+ const all = await this.selections(key);
130
+ for (const old of all.slice(this.limits.keptSelections))
131
+ await fs.rm(path.join(folder, old.selectionId + '.json'), { force: true });
132
+ return { selection };
133
+ }
134
+ // A short reading of one selection, for a message that carries it: what it is and its first rows.
135
+ async brief(ref, selectionId, rows) {
136
+ const read = await this.read({ ...ref, selection: selectionId, limit: rows });
137
+ const selection = read.selections.find(s => s.selectionId === selectionId);
138
+ const figure = read.figure;
139
+ return { cellId: figure.cellId, ...(figure.plot !== undefined ? { plot: figure.plot } : { path: figure.path, sha256: figure.sha256 }), cell: figure.cell.number, purpose: figure.cell.purpose, selectionId, count: selection.count, note: selection.note, layers: selection.layers,
140
+ rows: read.rows.map(r => r && ({ mark: r.mark, geom: r.geom, ...(Object.keys(r.facets).length ? { facets: r.facets } : {}), ...(r.values ? { values: r.values } : { built: r.built }) })) };
141
+ }
142
+ }
143
+ const content = [
144
+ 'Plots drawn in the project\'s live R session are kept as figures: every ggplot (ggplot2 and what is built on it, such as ggtree, enrichplot, Seurat and patchwork) is described after its cell, with each drawn point, bar, box, line or label bound to the built row it draws and, where the layer\'s statistic keeps its input rows, to the row of the plot\'s data. A page drawn with base graphics (plot, points, text, barplot, abline) is described from what it drew: each drawn part is a layer (points, text, rect, lines, abline) whose marks keep the values they were drawn at, in the data\'s units, and points or labels whose x and y are two columns of a table in the session (a data frame, a DataFrame, a matrix) are bound to its rows, directly or through -log10, log10, log2, log2(x + 1) or log1p; the layer\'s mapping names those columns (y: "-log10(padj)"), and its notes say how many marks were drawn elsewhere (clipped to the axis), or that an earlier layer of the plot draws the same column on the same axis through another transform, so the two layers place their marks on different scales. A ComplexHeatmap is described cell by cell: each cell names its row, column, value and slice, and its row of the matrix. A figure drawn with matplotlib in the live Python session (and what draws through it: seaborn, pandas .plot) is described from its artists: scatter points, line vertices, bars, heatmap cells (pcolormesh, seaborn heatmap and clustermap), segments, polygons and text labels are marks keeping the values they were drawn at; points, lines and labels whose x and y are two columns of a table in the session (a DataFrame, a Series, a 2-D array), directly or through -log10, log10, log2, log2(x + 1) or log1p, are bound to its rows, and the layer\'s mapping says which (y: "-log10(padj)"); a label drawn at a bound point names that point\'s row, or is bound by its text; a heatmap cell names its row, column and value, and its row of the session table drawn. A layer\'s notes say what the describer noticed: marks drawn elsewhere than their values, a column drawn on the same axis through another transform than an earlier layer of the plot draws it, or heatmap labels that name other rows than the values beside them; the cell\'s plot output carries them. A cell\'s plot output names its layers and marks when it was described, or figureProblem when it was not; other grid drawings and images (imshow) stay pictures. A ggplot the cell saved to a project file with ggsave(), a page drawn on a file the cell opened with png(), pdf() or svg() and closed with dev.off() (plotMA, a heatmap\'s draw(), a ggplot\'s print()), or a matplotlib figure saved with savefig(), is kept as a figure of that file too (the cell\'s saved list): name it by {path} instead of {cellId, plot} in both operations. It holds while the file keeps the bytes the cell saved; a file written again since is a picture.',
145
+ '',
146
+ '1. figure.read {cellId, plot} returns the summary: each layer (counted from 0) with its geom, stat, mapping, constants, the data source its marks came from, how many marks it drew and whether they are bound to rows or groups (and why not); the plots, panels and facets; legend scales; and each source\'s columns and key column. Use it to check that a plot shows what you meant: the marks drawn against the source\'s rows, a mapping, a layer drawn from computed values.',
147
+ '2. Rows behind marks: pass marks ["L.M"] (layer L, built row M), selection (the person\'s selection named in their message), or where [{column, op, value}] with op eq, ne, lt, le, gt, ge, in or contains, on a source column or a built column (count, x). Each row gives the mark\'s built values and its source row with the key column (a gene, a cell barcode, a tree node).',
148
+ '3. To point the person at marks, figure.select {cellId, plot, marks, note, changeId}: they are shown highlighted in the plot with your note. It changes neither the plot nor its data; draw a new plot to change what is shown.',
149
+ '',
150
+ 'A mark is named only within its figure. Speak about the same things across figures by the source key (gene, barcode, ID). An R figure is drawn again from the plot object at 7 × 5 in, so labels placed at random (ggrepel) may sit slightly differently from the picture; a matplotlib figure is drawn at its own size. A selected mark is what was drawn; what it means is still for you and the person to establish.',
151
+ ].join('\n');
152
+ export const plugin = {
153
+ entryVersion: '1.0', id: 'figures', package: '@rho-workspace/plugin-figures', version: '0.5.1', requires: ['kernel'], capabilities: ['figures'], presentation,
154
+ operations: Object.entries(operationContracts).map(([method, contract]) => ({ method, presentation: operationPresentation[method], version: '2.0', ...structuredClone(contract), requires: [...contract.requires] })),
155
+ skills: [{ id: 'figures.read', version: '1.0', content, digest: digest(content), presentation: skillPresentation,
156
+ intent: 'Read what a plot drew and the rows behind its marks, and point the person at marks.', requires: ['figure.read', 'figure.select'],
157
+ example: { method: 'figure.read', input: { cellId: 'k-0123456789abcdef', plot: 1, where: [{ column: 'padj', op: 'lt', value: 0.01 }], limit: 20 } },
158
+ failureRecipes: [
159
+ { kind: 'not_found', message: 'This plot is a picture only: nothing described what it draws.', next: 'Only ggplot2, base graphics and ComplexHeatmap pages, and matplotlib figures, drawn in a Notebook cell are described. Draw it with one of them to make its marks selectable, or read the picture with kernel.plot.' },
160
+ { kind: 'not_found', message: 'No Notebook cell saved a figure as this file with these bytes: it is a picture only.', next: 'Save the plot in a Notebook cell (kernel.run): ggsave(), png() or pdf() and dev.off() in R, savefig() in Python. A file written by a script outside the live session is not described, nor a PDF of several pages.' },
161
+ { kind: 'invalid', message: 'A mark names no layer or row of this figure.', next: 'Read the summary first; marks are "layer.row" counted from 0 within this figure.' },
162
+ ],
163
+ verify: { command: 'npm run test:figures', description: 'Describes real ggplot2, ggtree, Seurat-style and patchwork plots in R, and matplotlib and seaborn figures in Python, and reads, matches and selects their marks.' } }],
164
+ components: [],
165
+ };
@@ -0,0 +1,44 @@
1
+ export declare const selectionId: {
2
+ type: string;
3
+ pattern: string;
4
+ };
5
+ export declare const selectionSchema: {
6
+ type: string;
7
+ additionalProperties: boolean;
8
+ properties: Record<string, unknown>;
9
+ required: string[];
10
+ };
11
+ export declare const operationContracts: {
12
+ 'figure.read': {
13
+ inputSchema: {
14
+ type: string;
15
+ additionalProperties: boolean;
16
+ properties: Record<string, unknown>;
17
+ required: string[];
18
+ };
19
+ outputSchema: {
20
+ type: string;
21
+ additionalProperties: boolean;
22
+ properties: Record<string, unknown>;
23
+ required: string[];
24
+ };
25
+ requires: string[];
26
+ effect: "read";
27
+ };
28
+ 'figure.select': {
29
+ inputSchema: {
30
+ type: string;
31
+ additionalProperties: boolean;
32
+ properties: Record<string, unknown>;
33
+ required: string[];
34
+ };
35
+ outputSchema: {
36
+ type: string;
37
+ additionalProperties: boolean;
38
+ properties: Record<string, unknown>;
39
+ required: string[];
40
+ };
41
+ requires: string[];
42
+ effect: "metadata";
43
+ };
44
+ };