From 38050d0f8c18dc615a5d10d74a77fd57286e4c1b Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 17 Aug 2026 04:29:17 +0000 Subject: [PATCH 1/4] feat: move execution order into an Advanced section MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Opening a unit to edit its input file showed, first, a pointer to another unit. `Next` is a wiring detail — it says which unit runs after this one — and it sat above the Details and Input accordions, so the editor everyone opens the dialog for started below the fold. It keeps its place in the form, at the end and collapsed, beside the Details and Input accordions it now matches. The dialog opens on the input template. `dist/` is rebuilt to match, as it is tracked here. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01FFFpkKD3zHysJXgAbT2uYV --- dist/components/ExecutionUnit.js | 10 ++++----- src/components/ExecutionUnit.tsx | 35 ++++++++++++++++++++++---------- 2 files changed, 29 insertions(+), 16 deletions(-) diff --git a/dist/components/ExecutionUnit.js b/dist/components/ExecutionUnit.js index db4330a..d92bb49 100644 --- a/dist/components/ExecutionUnit.js +++ b/dist/components/ExecutionUnit.js @@ -161,10 +161,7 @@ export function ExecutionUnit({ unit, renderingContext, onUpdate, adjustable, ed }, }; }); - return (_jsxs(Stack, { spacing: 2, className: "ExecutionUnit", sx: { py: 2 }, children: [_jsx(Stack, { className: "ExecutionUnit-Stack", direction: "column", spacing: 2, children: UnitPointerFieldComponent && unitPointerKeys.map((key) => { - var _a; - return (_jsx(UnitPointerFieldComponent, { label: key, selectedValue: (_a = unit[key]) !== null && _a !== void 0 ? _a : "", availableUnits: availableUnits(), onChange: (value) => handleUnitKeyUpdate(key, value) }, key)); - }) }), _jsx(AccordionComponent, { className: "ExecutionUnit-Accordion", id: "execution-unit-details-accordion", header: "Details", disableGutters: true, children: _jsxs(Stack, { spacing: 1, children: [_jsx(Application, { editable: false, application: unit.application, executable: unit.executable, flavor: unit.flavor, showExec: true, showFlavor: true, onExecutableUpdate: onExecutableUpdate, onFlavorUpdate: onFlavorUpdate, isExecEditable: editable, isFlavorEditable: editable }), UnitDetailsComponent && (_jsx(UnitDetailsComponent, { unit: unit, editable: editable, onUnitResultsChanged: onUnitResultsChanged, onUnitIsDraftChanged: onUnitIsDraftChanged, onUnitMonitorChanged: onUnitMonitorChanged, onUnitPostProcessorChanged: onUnitPostProcessorChanged }))] }) }), _jsx(AccordionComponent, { isExpanded: true, header: "Input", disableGutters: true, className: "ExecutionUnit-Accordion unit-input-editor", children: _jsxs(DrawerContainer, { children: [_jsxs(MainContent, { children: [_jsx(TabsMenu, { tabs: tabs, activeTabIndex: activeTabIndex }), unit.input.map((inputRow, index) => { + return (_jsxs(Stack, { spacing: 2, className: "ExecutionUnit", sx: { py: 2 }, children: [_jsx(AccordionComponent, { className: "ExecutionUnit-Accordion", id: "execution-unit-details-accordion", header: "Details", disableGutters: true, children: _jsxs(Stack, { spacing: 1, children: [_jsx(Application, { editable: false, application: unit.application, executable: unit.executable, flavor: unit.flavor, showExec: true, showFlavor: true, onExecutableUpdate: onExecutableUpdate, onFlavorUpdate: onFlavorUpdate, isExecEditable: editable, isFlavorEditable: editable }), UnitDetailsComponent && (_jsx(UnitDetailsComponent, { unit: unit, editable: editable, onUnitResultsChanged: onUnitResultsChanged, onUnitIsDraftChanged: onUnitIsDraftChanged, onUnitMonitorChanged: onUnitMonitorChanged, onUnitPostProcessorChanged: onUnitPostProcessorChanged }))] }) }), _jsx(AccordionComponent, { isExpanded: true, header: "Input", disableGutters: true, className: "ExecutionUnit-Accordion unit-input-editor", children: _jsxs(DrawerContainer, { children: [_jsxs(MainContent, { children: [_jsx(TabsMenu, { tabs: tabs, activeTabIndex: activeTabIndex }), unit.input.map((inputRow, index) => { var _a; return (_jsx(ExecutionUnitInputFilePanel, { index: index, input: inputRow, isActive: index === activeTabIndex, activeInnerTabIndex: executionFileActiveTabIndex, onInnerTabChange: setExecutionFileActiveTabIndex, onTemplateTabClick: onTemplateClick, onPreviewTabClick: onPreviewClick, onContentUpdate: (content) => onUnitInputContentUpdate(index, content), onRenderedUpdate: (content) => onUnitInputRenderedUpdate(index, content), renderedContent: (_a = inputRow.rendered) !== null && _a !== void 0 ? _a : "", lineWrapping: lineWrapping, adjustable: adjustable, isStandalone: isStandalone }, `${unit.flowchartId}-input-${inputRow.template.name}`)); })] }), _jsxs(SideDrawer, { open: isDrawerVisible, children: [_jsxs(DrawerControlPanel, { children: [_jsx(DrawerControl, { isToggleAnchor: true, onClick: () => setIsDrawerVisible((v) => !v), icon: isDrawerVisible ? "shapes.arrow.right" : "shapes.arrow.left" }), _jsx(Divider, {}), _jsx(DrawerControl, { active: drawerContent === "context", onClick: () => setDrawerContent("context"), icon: "pages.context" }), _jsx(DrawerControl, { active: drawerContent === "materialList", onClick: () => setDrawerContent("materialList"), icon: "entities.material" })] }), _jsxs(DrawerContent, { open: isDrawerVisible, title: drawerContent === "context" ? "Context" : "Material", children: [drawerContent === "context" && (_jsx("pre", { className: "visible-rendering-context", style: { @@ -175,5 +172,8 @@ export function ExecutionUnit({ unit, renderingContext, onUpdate, adjustable, ed border: "none", }, children: JSON.stringify(renderingContext !== null && renderingContext !== void 0 ? renderingContext : {}, null, " ") })), drawerContent === "materialList" && (_jsx(List, { children: materials.map((material, index) => (_jsx(ListItem, { children: _jsx(FormControlLabel, { control: _jsx(Radio, { name: "material-options", id: material.id, value: material.id, checked: materialsIndex === index, onChange: () => { onMaterialSwitch === null || onMaterialSwitch === void 0 ? void 0 : onMaterialSwitch(index); - } }), label: _jsxs(Box, { children: [material.name, " (formula:", " ", _jsx("strong", { children: material.formula }), ", lattice:", " ", _jsxs("strong", { children: [material.lattice.type, ")"] })] }) }) }, `material-label-li-${material.id}`))) }, "material-label-ul"))] })] })] }) })] })); + } }), label: _jsxs(Box, { children: [material.name, " (formula:", " ", _jsx("strong", { children: material.formula }), ", lattice:", " ", _jsxs("strong", { children: [material.lattice.type, ")"] })] }) }) }, `material-label-li-${material.id}`))) }, "material-label-ul"))] })] })] }) }), UnitPointerFieldComponent && (_jsx(AccordionComponent, { className: "ExecutionUnit-Accordion", id: "execution-unit-advanced-accordion", header: "Advanced", disableGutters: true, children: _jsx(Stack, { className: "ExecutionUnit-Stack", direction: "column", spacing: 2, children: unitPointerKeys.map((key) => { + var _a; + return (_jsx(UnitPointerFieldComponent, { label: key, selectedValue: (_a = unit[key]) !== null && _a !== void 0 ? _a : "", availableUnits: availableUnits(), onChange: (value) => handleUnitKeyUpdate(key, value) }, key)); + }) }) }))] })); } diff --git a/src/components/ExecutionUnit.tsx b/src/components/ExecutionUnit.tsx index e893cbb..40ae35a 100644 --- a/src/components/ExecutionUnit.tsx +++ b/src/components/ExecutionUnit.tsx @@ -280,17 +280,6 @@ export function ExecutionUnit({ return ( - - {UnitPointerFieldComponent && unitPointerKeys.map((key) => ( - handleUnitKeyUpdate(key, value)} - /> - ))} - + {/* + Execution order is a wiring detail, not what someone opens a unit to change: it + used to sit above everything, so the first thing the input editor showed was a + pointer to another unit. It keeps its place in the form, at the end and collapsed. + */} + {UnitPointerFieldComponent && ( + + + {unitPointerKeys.map((key) => ( + handleUnitKeyUpdate(key, value)} + /> + ))} + + + )} ); } From b28348da7b63c93c17f2ff30391d15b29163e634 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 17 Aug 2026 06:51:09 +0000 Subject: [PATCH 2/4] feat: name the variables a template can use, and the ones that will render empty MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Templates render through nunjucks, which substitutes an unknown variable with the empty string. A typo does not fail, and does not even leave `{{ }}` behind in the preview — it quietly drops a value out of the input file the simulation then runs with. The only view of the context was a `
` of the whole thing as JSON, in whatever shape it happened
to be stored in.

The side drawer now lists what a template can write, grouped by where each value came
from — Important settings, from the material, set at job runtime — with the current
value beside it and a search box; clicking a row copies `{{ path }}`. Inserting at the
cursor needs the editor's EditorView, which cove's CodeMirror wrapper does not expose,
so copying is what this can honestly offer. Grouping is not guesswork: wode builds the
context as `{ ...providerName → data, ...externalContext }`, so anything outside the
external-context table came from a context provider.

Unresolved variables are reported above the template, with line numbers and a nearest-
name suggestion where one is close enough to be help rather than noise. Only `{{ … }}`
output is checked, never `{% if … %}` conditions — testing whether an optional value is
present is idiomatic, and these templates open by doing it. `{% raw %}` bodies are
skipped so job-runtime placeholders are not reported as defects, without shifting the
line numbers of anything after them.

Checked against every standata template — 184 of them, across 55 workflows — with zero
warnings raised, then against injected typos in a leaf, a root, a wrong provider and an
absent name, each caught on the right line.

Part of SOF-8024 portion 4, items 1.3 and 1.4.

Co-Authored-By: Claude Opus 5 
Claude-Session: https://claude.ai/code/session_01FFFpkKD3zHysJXgAbT2uYV
---
 dist/components/ExecutionUnit.d.ts            |   4 +-
 dist/components/ExecutionUnit.js              |  22 +-
 .../ExecutionUnitInputFilePanel.d.ts          |   5 +-
 .../components/ExecutionUnitInputFilePanel.js |   7 +-
 dist/components/TemplateVariablesPanel.d.ts   |  19 +
 dist/components/TemplateVariablesPanel.js     |  81 ++++
 dist/utils/templateVariables.d.ts             |  80 ++++
 dist/utils/templateVariables.js               | 308 ++++++++++++++
 src/components/ExecutionUnit.tsx              |  41 +-
 .../ExecutionUnitInputFilePanel.tsx           |  22 +
 src/components/TemplateVariablesPanel.tsx     | 160 ++++++++
 src/utils/templateVariables.ts                | 382 ++++++++++++++++++
 tests/templateVariables.tests.ts              | 184 +++++++++
 13 files changed, 1288 insertions(+), 27 deletions(-)
 create mode 100644 dist/components/TemplateVariablesPanel.d.ts
 create mode 100644 dist/components/TemplateVariablesPanel.js
 create mode 100644 dist/utils/templateVariables.d.ts
 create mode 100644 dist/utils/templateVariables.js
 create mode 100644 src/components/TemplateVariablesPanel.tsx
 create mode 100644 src/utils/templateVariables.ts
 create mode 100644 tests/templateVariables.tests.ts

diff --git a/dist/components/ExecutionUnit.d.ts b/dist/components/ExecutionUnit.d.ts
index 31f1e1e..026d59f 100644
--- a/dist/components/ExecutionUnit.d.ts
+++ b/dist/components/ExecutionUnit.d.ts
@@ -21,6 +21,8 @@ export type ExecutionUnitProps = {
         availableUnits: AnySubworkflowUnit[];
         onChange: (value: string) => void;
     }>;
+    /** Labels for top-level rendering-context keys, when the host knows better than the default. */
+    variableOriginOverrides?: Record;
     /** Injected component for unit details (results, monitors, post-processors). */
     UnitDetailsComponent?: React.ComponentType<{
         unit: ExecutionUnitSchema;
@@ -31,4 +33,4 @@ export type ExecutionUnitProps = {
         onUnitPostProcessorChanged: (postProcessor: string, enabled: boolean) => void;
     }>;
 };
-export declare function ExecutionUnit({ unit, renderingContext, onUpdate, adjustable, editable, isStandalone, materials, materialsIndex, onMaterialSwitch, units, UnitPointerFieldComponent, UnitDetailsComponent, }: ExecutionUnitProps): React.JSX.Element;
+export declare function ExecutionUnit({ unit, renderingContext, onUpdate, adjustable, editable, isStandalone, materials, materialsIndex, onMaterialSwitch, units, UnitPointerFieldComponent, UnitDetailsComponent, variableOriginOverrides, }: ExecutionUnitProps): React.JSX.Element;
diff --git a/dist/components/ExecutionUnit.js b/dist/components/ExecutionUnit.js
index d92bb49..131ac9d 100644
--- a/dist/components/ExecutionUnit.js
+++ b/dist/components/ExecutionUnit.js
@@ -12,16 +12,18 @@ import List from "@mui/material/List";
 import ListItem from "@mui/material/ListItem";
 import Radio from "@mui/material/Radio";
 import Stack from "@mui/material/Stack";
-import { useCallback, useEffect, useState } from "react";
+import { useCallback, useEffect, useMemo, useState } from "react";
 import { Application } from "./Application";
 import { ExecutionUnitInputFilePanel, } from "./ExecutionUnitInputFilePanel";
+import { TemplateVariablesPanel } from "./TemplateVariablesPanel";
 import { DrawerContainer, DrawerContent, DrawerControl, DrawerControlPanel, MainContent, SideDrawer, } from "./SideDrawer";
 import TabsMenu from "@mat3ra/cove/dist/mui/components/tabs/TabsMenu";
+import { findUnresolvedVariables } from "../utils/templateVariables";
 const unitPointerKeys = ["next"];
 function runtimeItemToNameResult(item) {
     return typeof item === "string" ? safeMakeObject(item) : item;
 }
-export function ExecutionUnit({ unit, renderingContext, onUpdate, adjustable, editable = true, isStandalone, materials = [], materialsIndex, onMaterialSwitch, units = [], UnitPointerFieldComponent, UnitDetailsComponent, }) {
+export function ExecutionUnit({ unit, renderingContext, onUpdate, adjustable, editable = true, isStandalone, materials = [], materialsIndex, onMaterialSwitch, units = [], UnitPointerFieldComponent, UnitDetailsComponent, variableOriginOverrides, }) {
     const [drawerContent, setDrawerContent] = useState("context");
     const [isDrawerVisible, setIsDrawerVisible] = useState(false);
     const [lineWrapping] = useState(true);
@@ -145,6 +147,12 @@ export function ExecutionUnit({ unit, renderingContext, onUpdate, adjustable, ed
         unitInstance.setProp(key, value);
         onUpdate(unitInstance.toJSON());
     }, [unit, onUpdate]);
+    /**
+     * nunjucks substitutes an unknown variable with the empty string, so a typo does not fail —
+     * it quietly drops a value out of the input file. Checked per input so the warning can sit
+     * with the template it belongs to.
+     */
+    const issuesByInput = useMemo(() => unit.input.map((inputRow) => { var _a; return findUnresolvedVariables((_a = inputRow.template) === null || _a === void 0 ? void 0 : _a.content, renderingContext); }), [unit.input, renderingContext]);
     const tabs = unit.input.map((inputRow, index) => {
         const label = inputRow.template.name;
         return {
@@ -163,14 +171,8 @@ export function ExecutionUnit({ unit, renderingContext, onUpdate, adjustable, ed
     });
     return (_jsxs(Stack, { spacing: 2, className: "ExecutionUnit", sx: { py: 2 }, children: [_jsx(AccordionComponent, { className: "ExecutionUnit-Accordion", id: "execution-unit-details-accordion", header: "Details", disableGutters: true, children: _jsxs(Stack, { spacing: 1, children: [_jsx(Application, { editable: false, application: unit.application, executable: unit.executable, flavor: unit.flavor, showExec: true, showFlavor: true, onExecutableUpdate: onExecutableUpdate, onFlavorUpdate: onFlavorUpdate, isExecEditable: editable, isFlavorEditable: editable }), UnitDetailsComponent && (_jsx(UnitDetailsComponent, { unit: unit, editable: editable, onUnitResultsChanged: onUnitResultsChanged, onUnitIsDraftChanged: onUnitIsDraftChanged, onUnitMonitorChanged: onUnitMonitorChanged, onUnitPostProcessorChanged: onUnitPostProcessorChanged }))] }) }), _jsx(AccordionComponent, { isExpanded: true, header: "Input", disableGutters: true, className: "ExecutionUnit-Accordion unit-input-editor", children: _jsxs(DrawerContainer, { children: [_jsxs(MainContent, { children: [_jsx(TabsMenu, { tabs: tabs, activeTabIndex: activeTabIndex }), unit.input.map((inputRow, index) => {
                                     var _a;
-                                    return (_jsx(ExecutionUnitInputFilePanel, { index: index, input: inputRow, isActive: index === activeTabIndex, activeInnerTabIndex: executionFileActiveTabIndex, onInnerTabChange: setExecutionFileActiveTabIndex, onTemplateTabClick: onTemplateClick, onPreviewTabClick: onPreviewClick, onContentUpdate: (content) => onUnitInputContentUpdate(index, content), onRenderedUpdate: (content) => onUnitInputRenderedUpdate(index, content), renderedContent: (_a = inputRow.rendered) !== null && _a !== void 0 ? _a : "", lineWrapping: lineWrapping, adjustable: adjustable, isStandalone: isStandalone }, `${unit.flowchartId}-input-${inputRow.template.name}`));
-                                })] }), _jsxs(SideDrawer, { open: isDrawerVisible, children: [_jsxs(DrawerControlPanel, { children: [_jsx(DrawerControl, { isToggleAnchor: true, onClick: () => setIsDrawerVisible((v) => !v), icon: isDrawerVisible ? "shapes.arrow.right" : "shapes.arrow.left" }), _jsx(Divider, {}), _jsx(DrawerControl, { active: drawerContent === "context", onClick: () => setDrawerContent("context"), icon: "pages.context" }), _jsx(DrawerControl, { active: drawerContent === "materialList", onClick: () => setDrawerContent("materialList"), icon: "entities.material" })] }), _jsxs(DrawerContent, { open: isDrawerVisible, title: drawerContent === "context" ? "Context" : "Material", children: [drawerContent === "context" && (_jsx("pre", { className: "visible-rendering-context", style: {
-                                                whiteSpace: "pre-wrap",
-                                                margin: 0,
-                                                padding: 10,
-                                                minHeight: "100%",
-                                                border: "none",
-                                            }, children: JSON.stringify(renderingContext !== null && renderingContext !== void 0 ? renderingContext : {}, null, "    ") })), drawerContent === "materialList" && (_jsx(List, { children: materials.map((material, index) => (_jsx(ListItem, { children: _jsx(FormControlLabel, { control: _jsx(Radio, { name: "material-options", id: material.id, value: material.id, checked: materialsIndex === index, onChange: () => {
+                                    return (_jsx(ExecutionUnitInputFilePanel, { index: index, input: inputRow, isActive: index === activeTabIndex, activeInnerTabIndex: executionFileActiveTabIndex, onInnerTabChange: setExecutionFileActiveTabIndex, onTemplateTabClick: onTemplateClick, onPreviewTabClick: onPreviewClick, onContentUpdate: (content) => onUnitInputContentUpdate(index, content), onRenderedUpdate: (content) => onUnitInputRenderedUpdate(index, content), renderedContent: (_a = inputRow.rendered) !== null && _a !== void 0 ? _a : "", lineWrapping: lineWrapping, adjustable: adjustable, isStandalone: isStandalone, issues: issuesByInput[index] }, `${unit.flowchartId}-input-${inputRow.template.name}`));
+                                })] }), _jsxs(SideDrawer, { open: isDrawerVisible, children: [_jsxs(DrawerControlPanel, { children: [_jsx(DrawerControl, { isToggleAnchor: true, "data-tid": "unit-drawer-toggle", onClick: () => setIsDrawerVisible((v) => !v), icon: isDrawerVisible ? "shapes.arrow.right" : "shapes.arrow.left" }), _jsx(Divider, {}), _jsx(DrawerControl, { active: drawerContent === "context", "data-tid": "unit-drawer-variables", onClick: () => setDrawerContent("context"), icon: "pages.context" }), _jsx(DrawerControl, { active: drawerContent === "materialList", "data-tid": "unit-drawer-material", onClick: () => setDrawerContent("materialList"), icon: "entities.material" })] }), _jsxs(DrawerContent, { open: isDrawerVisible, title: drawerContent === "context" ? "Variables" : "Material", children: [drawerContent === "context" && (_jsx(TemplateVariablesPanel, { renderingContext: renderingContext, originOverrides: variableOriginOverrides })), drawerContent === "materialList" && (_jsx(List, { children: materials.map((material, index) => (_jsx(ListItem, { children: _jsx(FormControlLabel, { control: _jsx(Radio, { name: "material-options", id: material.id, value: material.id, checked: materialsIndex === index, onChange: () => {
                                                             onMaterialSwitch === null || onMaterialSwitch === void 0 ? void 0 : onMaterialSwitch(index);
                                                         } }), label: _jsxs(Box, { children: [material.name, " (formula:", " ", _jsx("strong", { children: material.formula }), ", lattice:", " ", _jsxs("strong", { children: [material.lattice.type, ")"] })] }) }) }, `material-label-li-${material.id}`))) }, "material-label-ul"))] })] })] }) }), UnitPointerFieldComponent && (_jsx(AccordionComponent, { className: "ExecutionUnit-Accordion", id: "execution-unit-advanced-accordion", header: "Advanced", disableGutters: true, children: _jsx(Stack, { className: "ExecutionUnit-Stack", direction: "column", spacing: 2, children: unitPointerKeys.map((key) => {
                         var _a;
diff --git a/dist/components/ExecutionUnitInputFilePanel.d.ts b/dist/components/ExecutionUnitInputFilePanel.d.ts
index 67a3e6b..6860f89 100644
--- a/dist/components/ExecutionUnitInputFilePanel.d.ts
+++ b/dist/components/ExecutionUnitInputFilePanel.d.ts
@@ -1,5 +1,6 @@
 import type { ExecutionUnitInputItemSchema } from "@mat3ra/esse/dist/js/types";
 import React from "react";
+import type { TemplateIssue } from "../utils/templateVariables";
 /** `input[]` row for an execution unit; optional `name` is set by `setInputItemNameByIndex` for tab labels. */
 export type ExecutionUnitInput = ExecutionUnitInputItemSchema & {
     name?: string;
@@ -18,6 +19,8 @@ type ExecutionUnitInputFilePanelProps = {
     lineWrapping: boolean;
     adjustable?: boolean;
     isStandalone?: boolean;
+    /** Variables in this template that will render to nothing. */
+    issues?: TemplateIssue[];
 };
-export declare function ExecutionUnitInputFilePanel({ index, input, isActive, activeInnerTabIndex, onInnerTabChange, onTemplateTabClick, onPreviewTabClick, onContentUpdate, onRenderedUpdate, renderedContent, lineWrapping, adjustable, isStandalone, }: ExecutionUnitInputFilePanelProps): React.JSX.Element;
+export declare function ExecutionUnitInputFilePanel({ index, input, isActive, activeInnerTabIndex, onInnerTabChange, onTemplateTabClick, onPreviewTabClick, onContentUpdate, onRenderedUpdate, renderedContent, lineWrapping, adjustable, isStandalone, issues, }: ExecutionUnitInputFilePanelProps): React.JSX.Element;
 export {};
diff --git a/dist/components/ExecutionUnitInputFilePanel.js b/dist/components/ExecutionUnitInputFilePanel.js
index e10a10a..ae2b641 100644
--- a/dist/components/ExecutionUnitInputFilePanel.js
+++ b/dist/components/ExecutionUnitInputFilePanel.js
@@ -1,5 +1,6 @@
 import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
 import CodeMirror from "@mat3ra/cove/dist/other/codemirror";
+import Alert from "@mui/material/Alert";
 import { getProgrammingLanguageFromFileExtension } from "@mat3ra/code/dist/js/utils";
 import Stack from "@mui/material/Stack";
 import TabsMenu from "@mat3ra/cove/dist/mui/components/tabs/TabsMenu";
@@ -10,7 +11,7 @@ const codeMirrorDefaults = {
     viewportMargin: Infinity,
     minHeight: 70,
 };
-export function ExecutionUnitInputFilePanel({ index, input, isActive, activeInnerTabIndex, onInnerTabChange, onTemplateTabClick, onPreviewTabClick, onContentUpdate, onRenderedUpdate, renderedContent, lineWrapping, adjustable, isStandalone, }) {
+export function ExecutionUnitInputFilePanel({ index, input, isActive, activeInnerTabIndex, onInnerTabChange, onTemplateTabClick, onPreviewTabClick, onContentUpdate, onRenderedUpdate, renderedContent, lineWrapping, adjustable, isStandalone, issues = [], }) {
     const contentTabIdString = `template-${index}`;
     const previewTabIdString = `preview-${index}`;
     const fileTabs = [
@@ -35,7 +36,9 @@ export function ExecutionUnitInputFilePanel({ index, input, isActive, activeInne
             dataName: previewTabIdString,
         },
     ];
-    return (_jsxs(Stack, { display: isActive ? undefined : "none", spacing: 2, id: String(index), className: `ExecutionFile ${isActive ? "active" : ""}`, children: [_jsx(TabsMenu, { tabs: fileTabs, activeTabIndex: activeInnerTabIndex, sx: { fontSize: 12 } }), _jsx(Stack, { display: activeInnerTabIndex === 0 ? undefined : "none", spacing: 2, id: contentTabIdString, className: `ContentTabPane ${activeInnerTabIndex === 0 ? "active" : ""}`, children: _jsx(CodeMirror, { content: input.template.content, updateContent: onContentUpdate, language: getProgrammingLanguageFromFileExtension(input.template.name, "jinja2"), options: {
+    return (_jsxs(Stack, { display: isActive ? undefined : "none", spacing: 2, id: String(index), className: `ExecutionFile ${isActive ? "active" : ""}`, children: [_jsx(TabsMenu, { tabs: fileTabs, activeTabIndex: activeInnerTabIndex, sx: { fontSize: 12 } }), issues.length > 0 && (_jsx(Alert, { severity: "warning", "data-tid": "template-issues", sx: { py: 0.25 }, children: issues
+                    .map((issue) => `line ${issue.line}: ${issue.name}${issue.suggestion ? ` — did you mean ${issue.suggestion}?` : ""}`)
+                    .join("  ·  ") })), _jsx(Stack, { display: activeInnerTabIndex === 0 ? undefined : "none", spacing: 2, id: contentTabIdString, className: `ContentTabPane ${activeInnerTabIndex === 0 ? "active" : ""}`, children: _jsx(CodeMirror, { content: input.template.content, updateContent: onContentUpdate, language: getProgrammingLanguageFromFileExtension(input.template.name, "jinja2"), options: {
                         ...codeMirrorDefaults,
                         autoSave: true,
                         lineWrapping,
diff --git a/dist/components/TemplateVariablesPanel.d.ts b/dist/components/TemplateVariablesPanel.d.ts
new file mode 100644
index 0000000..3202f8a
--- /dev/null
+++ b/dist/components/TemplateVariablesPanel.d.ts
@@ -0,0 +1,19 @@
+import React from "react";
+export interface TemplateVariablesPanelProps {
+    renderingContext?: Record;
+    /** Host-supplied labels for top-level context keys; see `describeOrigin`. */
+    originOverrides?: Record;
+}
+/**
+ * What a template can write, and where each value came from.
+ *
+ * Replaces a `
` of the whole rendering context as JSON — everything available, in the shape
+ * it happened to be stored in, with no indication of which parts came from the Settings tab and
+ * which arrive at job runtime. Clicking a row copies the `{{ … }}` expression: inserting at the
+ * cursor needs the editor's `EditorView`, which cove's CodeMirror wrapper does not expose.
+ *
+ * Unresolved variables are reported by {@link ExecutionUnitInputFilePanel}, directly above the
+ * template they are in, rather than here as well.
+ */
+export declare function TemplateVariablesPanel({ renderingContext, originOverrides, }: TemplateVariablesPanelProps): React.JSX.Element;
+export default TemplateVariablesPanel;
diff --git a/dist/components/TemplateVariablesPanel.js b/dist/components/TemplateVariablesPanel.js
new file mode 100644
index 0000000..7061ee5
--- /dev/null
+++ b/dist/components/TemplateVariablesPanel.js
@@ -0,0 +1,81 @@
+import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
+import Box from "@mui/material/Box";
+import InputAdornment from "@mui/material/InputAdornment";
+import Stack from "@mui/material/Stack";
+import TextField from "@mui/material/TextField";
+import Tooltip from "@mui/material/Tooltip";
+import Typography from "@mui/material/Typography";
+import { useMemo, useState } from "react";
+import { flattenRenderingContext } from "../utils/templateVariables";
+/** "renders with: Si … · espresso", from the context itself rather than a separate prop. */
+function describeRenderTarget(context) {
+    const read = (key) => {
+        var _a;
+        const value = ((_a = context === null || context === void 0 ? void 0 : context[key]) !== null && _a !== void 0 ? _a : {});
+        return typeof (value === null || value === void 0 ? void 0 : value.name) === "string" ? value.name : undefined;
+    };
+    return [read("material"), read("application")].filter(Boolean).join(" · ");
+}
+function groupByOrigin(variables) {
+    const groups = new Map();
+    variables.forEach((variable) => {
+        const bucket = groups.get(variable.origin);
+        if (bucket)
+            bucket.push(variable);
+        else
+            groups.set(variable.origin, [variable]);
+    });
+    return [...groups.entries()];
+}
+/**
+ * What a template can write, and where each value came from.
+ *
+ * Replaces a `
` of the whole rendering context as JSON — everything available, in the shape
+ * it happened to be stored in, with no indication of which parts came from the Settings tab and
+ * which arrive at job runtime. Clicking a row copies the `{{ … }}` expression: inserting at the
+ * cursor needs the editor's `EditorView`, which cove's CodeMirror wrapper does not expose.
+ *
+ * Unresolved variables are reported by {@link ExecutionUnitInputFilePanel}, directly above the
+ * template they are in, rather than here as well.
+ */
+export function TemplateVariablesPanel({ renderingContext, originOverrides, }) {
+    const [search, setSearch] = useState("");
+    const [copied, setCopied] = useState(null);
+    const variables = useMemo(() => flattenRenderingContext(renderingContext, { originOverrides }), [renderingContext, originOverrides]);
+    const needle = search.trim().toLowerCase();
+    const visible = needle
+        ? variables.filter((variable) => variable.path.toLowerCase().includes(needle) ||
+            variable.preview.toLowerCase().includes(needle))
+        : variables;
+    const copy = (path) => {
+        var _a;
+        const expression = `{{ ${path} }}`;
+        (_a = navigator.clipboard) === null || _a === void 0 ? void 0 : _a.writeText(expression).catch(() => undefined);
+        setCopied(path);
+        // Fire-and-forget; the panel only needs the label to settle back.
+        setTimeout(() => setCopied((current) => (current === path ? null : current)), 1200);
+    };
+    const renderTarget = describeRenderTarget(renderingContext);
+    return (_jsxs(Stack, { spacing: 1.5, sx: { p: 1.5 }, "data-tid": "template-variables-panel", children: [renderTarget ? (_jsxs(Typography, { variant: "caption", color: "text.secondary", children: ["renders with: ", renderTarget] })) : null, _jsx(TextField, { size: "small", value: search, onChange: (event) => setSearch(event.target.value), placeholder: "Search variables", InputProps: {
+                    startAdornment: _jsx(InputAdornment, { position: "start", children: "\u2315" }),
+                    inputProps: { "data-tid": "template-variables-search" },
+                } }), visible.length === 0 ? (_jsx(Typography, { variant: "body2", color: "text.secondary", children: variables.length === 0
+                    ? "No context to render this template with yet."
+                    : `Nothing matches “${search}”.` })) : (groupByOrigin(visible).map(([origin, rows]) => (_jsxs(Box, { children: [_jsx(Typography, { variant: "overline", color: "text.secondary", children: origin }), rows.map((variable) => (_jsx(Tooltip, { title: copied === variable.path
+                            ? "Copied"
+                            : `Copy {{ ${variable.path} }}`, children: _jsxs(Box, { onClick: () => copy(variable.path), "data-tid": `template-variable-${variable.path}`, sx: {
+                                display: "flex",
+                                gap: 1,
+                                alignItems: "baseline",
+                                px: 0.75,
+                                py: 0.25,
+                                borderRadius: 0.5,
+                                cursor: "pointer",
+                                "&:hover": { backgroundColor: "action.hover" },
+                            }, children: [_jsx(Typography, { variant: "caption", sx: {
+                                        fontFamily: "monospace",
+                                        fontWeight: variable.isLeaf ? 400 : 600,
+                                        whiteSpace: "nowrap",
+                                    }, children: variable.path }), _jsx(Typography, { variant: "caption", color: "text.secondary", noWrap: true, sx: { minWidth: 0 }, children: variable.preview })] }) }, variable.path)))] }, origin))))] }));
+}
+export default TemplateVariablesPanel;
diff --git a/dist/utils/templateVariables.d.ts b/dist/utils/templateVariables.d.ts
new file mode 100644
index 0000000..158bf90
--- /dev/null
+++ b/dist/utils/templateVariables.d.ts
@@ -0,0 +1,80 @@
+/**
+ * Reading an input template against the context it will render with.
+ *
+ * Two questions the editor could not answer before: what is available to write into a template,
+ * and whether what is written actually resolves. The second matters more than it looks —
+ * templates render through nunjucks, which substitutes an unknown variable with the **empty
+ * string**. A typo does not fail loudly, or even leave `{{ }}` behind in the preview; it quietly
+ * drops a value out of the input file a simulation then runs with.
+ *
+ * Kept free of React so the parsing rules are testable on their own.
+ */
+/** One entry of the rendering context, as the variables panel lists it. */
+export interface ContextVariable {
+    /** Dotted path to write into a template, e.g. `cutoffs.wavefunction`. */
+    path: string;
+    /** Top-level key the path hangs off, which is what determines its origin. */
+    root: string;
+    origin: string;
+    /** Short rendering of the current value. */
+    preview: string;
+    /** False for objects and arrays, which are containers rather than values. */
+    isLeaf: boolean;
+}
+/** A `{{ … }}` expression that will render to nothing. */
+export interface TemplateIssue {
+    /** 1-based, so it matches the editor's gutter. */
+    line: number;
+    /** The whole expression, for showing what was written. */
+    expression: string;
+    /** The longest prefix of the path that does not resolve. */
+    name: string;
+    /** Nearest available name, when one is close enough to be worth offering. */
+    suggestion?: string;
+}
+export declare function describeOrigin(root: string, overrides?: Record): string;
+export interface FlattenOptions {
+    /** How far to walk into nested plain objects. Deeper paths are still writable by hand. */
+    maxDepth?: number;
+    /** Backstop for a context that turns out to be far larger than expected. */
+    maxVariables?: number;
+    originOverrides?: Record;
+}
+/** Every path a template can write, in the order the context declares them. */
+export declare function flattenRenderingContext(context: Record | undefined, { maxDepth, maxVariables, originOverrides }?: FlattenOptions): ContextVariable[];
+/** Names the template binds itself, which are not expected to be in the context. */
+export declare function collectLocalNames(template: string): Set;
+/**
+ * Root-anchored dotted paths referenced by an expression.
+ *
+ * Skips string literals, filter names after `|`, property names after `.` whose root was already
+ * skipped, and anything called as a function. Stops a path at `[`, so a dynamic index such as
+ * `input.perMaterial[subworkflowContext.MATERIAL_INDEX]` contributes `input.perMaterial` and the
+ * index expression separately, rather than one path that could never be resolved.
+ */
+export declare function extractPaths(expression: string): string[];
+interface Resolution {
+    resolved: boolean;
+    /** Longest prefix that failed, when it did. */
+    missing?: string;
+    /** Keys available where it failed, for suggesting a near match. */
+    available?: string[];
+}
+/**
+ * Walks a dotted path through the context, stopping the moment it reaches something that is not
+ * plain data — an entity instance's own properties cannot be enumerated reliably, and reporting
+ * `material.name` as missing because `Material` is a class would be worse than saying nothing.
+ */
+export declare function resolvePath(context: Record, path: string): Resolution;
+/** Nearest candidate, when one is near enough that offering it is help rather than noise. */
+export declare function suggestName(name: string, candidates: string[]): string | undefined;
+/**
+ * Expressions in `template` that will not resolve against `context`.
+ *
+ * Only `{{ … }}` output is checked, never `{% if … %}` conditions: testing whether an optional
+ * value is present is idiomatic — the templates here open with
+ * `{% if subworkflowContext.MATERIAL_INDEX %}` — while an unresolved `{{ … }}` silently writes an
+ * empty string into the file the simulation runs on.
+ */
+export declare function findUnresolvedVariables(template: string | undefined, context: Record | undefined): TemplateIssue[];
+export {};
diff --git a/dist/utils/templateVariables.js b/dist/utils/templateVariables.js
new file mode 100644
index 0000000..2556344
--- /dev/null
+++ b/dist/utils/templateVariables.js
@@ -0,0 +1,308 @@
+/**
+ * Reading an input template against the context it will render with.
+ *
+ * Two questions the editor could not answer before: what is available to write into a template,
+ * and whether what is written actually resolves. The second matters more than it looks —
+ * templates render through nunjucks, which substitutes an unknown variable with the **empty
+ * string**. A typo does not fail loudly, or even leave `{{ }}` behind in the preview; it quietly
+ * drops a value out of the input file a simulation then runs with.
+ *
+ * Kept free of React so the parsing rules are testable on their own.
+ */
+/**
+ * Where a top-level context key comes from.
+ *
+ * wode builds the rendering context as `{ ...providerName → providerData, ...externalContext }`
+ * (`ExecutionUnit.saveRenderingContext`), so anything **not** in this table arrived from a
+ * context provider — which is what the Settings tab edits. That is why the fallback below is a
+ * statement rather than a guess.
+ */
+const EXTERNAL_CONTEXT_ORIGINS = {
+    material: "From the material",
+    materials: "From the material",
+    materialsSet: "From the material",
+    input: "Derived from the material",
+    application: "From the application",
+    methodData: "From the method",
+    subworkflowContext: "Set at job runtime",
+    workflowHasRelaxation: "From the workflow",
+    jobHasParent: "From the workflow",
+};
+const PROVIDER_ORIGIN = "Important settings";
+export function describeOrigin(root, overrides) {
+    var _a, _b;
+    return (_b = (_a = overrides === null || overrides === void 0 ? void 0 : overrides[root]) !== null && _a !== void 0 ? _a : EXTERNAL_CONTEXT_ORIGINS[root]) !== null && _b !== void 0 ? _b : PROVIDER_ORIGIN;
+}
+/** Entity instances (Material, Application…) are opaque here: only plain data is walked. */
+function isPlainObject(value) {
+    if (typeof value !== "object" || value === null || Array.isArray(value))
+        return false;
+    const prototype = Object.getPrototypeOf(value);
+    return prototype === Object.prototype || prototype === null;
+}
+function previewValue(value) {
+    if (value === null)
+        return "null";
+    if (value === undefined)
+        return "undefined";
+    if (Array.isArray(value))
+        return `[${value.length} item${value.length === 1 ? "" : "s"}]`;
+    if (typeof value === "object") {
+        const keys = Object.keys(value);
+        return keys.length
+            ? `{ ${keys.slice(0, 3).join(", ")}${keys.length > 3 ? ", …" : ""} }`
+            : "{ }";
+    }
+    const text = String(value);
+    return text.length > 60 ? `${text.slice(0, 57)}…` : text;
+}
+/** Every path a template can write, in the order the context declares them. */
+export function flattenRenderingContext(context, { maxDepth = 3, maxVariables = 400, originOverrides } = {}) {
+    if (!context || typeof context !== "object")
+        return [];
+    const variables = [];
+    const walk = (value, path, root, depth) => {
+        if (variables.length >= maxVariables)
+            return;
+        const isLeaf = !isPlainObject(value) && !Array.isArray(value);
+        variables.push({
+            path,
+            root,
+            origin: describeOrigin(root, originOverrides),
+            preview: previewValue(value),
+            isLeaf,
+        });
+        if (isPlainObject(value) && depth < maxDepth) {
+            Object.keys(value).forEach((key) => walk(value[key], `${path}.${key}`, root, depth + 1));
+        }
+    };
+    Object.keys(context).forEach((key) => walk(context[key], key, key, 1));
+    return variables;
+}
+// ---------------------------------------------------------------------------
+// Template analysis
+// ---------------------------------------------------------------------------
+/** Names nunjucks provides itself, plus the operators that read like identifiers. */
+const RESERVED = new Set([
+    "loop",
+    "range",
+    "cycler",
+    "joiner",
+    "super",
+    "self",
+    "true",
+    "false",
+    "none",
+    "null",
+    "True",
+    "False",
+    "None",
+    "if",
+    "else",
+    "elif",
+    "not",
+    "and",
+    "or",
+    "in",
+    "is",
+    "by",
+]);
+/**
+ * Blanks out `{% raw %}` bodies, preserving offsets and line breaks.
+ *
+ * What is inside them is deliberately *not* rendered — job-runtime placeholders such as
+ * `{{ JOB_WORK_DIR }}` — so checking them would report the template's whole point as a defect.
+ */
+function blankRawBlocks(template) {
+    return template.replace(/\{%-?\s*raw\s*-?%\}[\s\S]*?\{%-?\s*endraw\s*-?%\}/g, (block) => block.replace(/[^\n]/g, " "));
+}
+/** Names the template binds itself, which are not expected to be in the context. */
+export function collectLocalNames(template) {
+    const names = new Set();
+    const add = (name) => {
+        const trimmed = name.trim();
+        if (trimmed)
+            names.add(trimmed);
+    };
+    Array.from(template.matchAll(/\{%-?\s*(?:set|with)\s+([A-Za-z_]\w*)/g)).forEach((match) => add(match[1]));
+    Array.from(template.matchAll(/\{%-?\s*macro\s+([A-Za-z_]\w*)/g)).forEach((match) => add(match[1]));
+    // `{% for a, b in items %}` binds both names.
+    Array.from(template.matchAll(/\{%-?\s*for\s+([\s\S]*?)\s+in\s/g)).forEach((match) => match[1].split(",").forEach(add));
+    return names;
+}
+/** The `{{ … }}` expressions, with the offset each starts at. */
+function extractOutputExpressions(template) {
+    return Array.from(template.matchAll(/\{\{([\s\S]*?)\}\}/g)).map((match) => {
+        var _a;
+        return ({
+            expression: match[1],
+            index: (_a = match.index) !== null && _a !== void 0 ? _a : 0,
+        });
+    });
+}
+/**
+ * Root-anchored dotted paths referenced by an expression.
+ *
+ * Skips string literals, filter names after `|`, property names after `.` whose root was already
+ * skipped, and anything called as a function. Stops a path at `[`, so a dynamic index such as
+ * `input.perMaterial[subworkflowContext.MATERIAL_INDEX]` contributes `input.perMaterial` and the
+ * index expression separately, rather than one path that could never be resolved.
+ */
+export function extractPaths(expression) {
+    const paths = [];
+    let index = 0;
+    let previous = "";
+    while (index < expression.length) {
+        const char = expression[index];
+        if (char === "'" || char === '"') {
+            const quote = char;
+            index += 1;
+            while (index < expression.length && expression[index] !== quote) {
+                index += expression[index] === "\\" ? 2 : 1;
+            }
+            index += 1;
+            previous = "literal";
+            continue;
+        }
+        if (/\s/.test(char)) {
+            index += 1;
+            continue;
+        }
+        if (!/[A-Za-z_]/.test(char)) {
+            previous = char;
+            index += 1;
+            continue;
+        }
+        let end = index;
+        while (end < expression.length && /\w/.test(expression[end]))
+            end += 1;
+        const identifier = expression.slice(index, end);
+        let afterIdentifier = end;
+        while (afterIdentifier < expression.length && /\s/.test(expression[afterIdentifier])) {
+            afterIdentifier += 1;
+        }
+        const isCall = expression[afterIdentifier] === "(";
+        const isSkipped = previous === "|" || previous === "." || isCall || RESERVED.has(identifier);
+        if (isSkipped) {
+            index = end;
+            previous = "identifier";
+            continue;
+        }
+        let path = identifier;
+        let cursor = end;
+        while (expression[cursor] === ".") {
+            let segmentEnd = cursor + 1;
+            while (segmentEnd < expression.length && /\w/.test(expression[segmentEnd])) {
+                segmentEnd += 1;
+            }
+            if (segmentEnd === cursor + 1)
+                break;
+            let afterSegment = segmentEnd;
+            while (afterSegment < expression.length && /\s/.test(expression[afterSegment])) {
+                afterSegment += 1;
+            }
+            // A method call is not a context path.
+            if (expression[afterSegment] === "(")
+                break;
+            path += `.${expression.slice(cursor + 1, segmentEnd)}`;
+            cursor = segmentEnd;
+        }
+        paths.push(path);
+        index = cursor;
+        previous = "identifier";
+    }
+    return paths;
+}
+/**
+ * Walks a dotted path through the context, stopping the moment it reaches something that is not
+ * plain data — an entity instance's own properties cannot be enumerated reliably, and reporting
+ * `material.name` as missing because `Material` is a class would be worse than saying nothing.
+ */
+export function resolvePath(context, path) {
+    const segments = path.split(".");
+    let current = context;
+    for (let i = 0; i < segments.length; i += 1) {
+        if (!isPlainObject(current)) {
+            return { resolved: true };
+        }
+        if (!(segments[i] in current)) {
+            return {
+                resolved: false,
+                missing: segments.slice(0, i + 1).join("."),
+                available: Object.keys(current),
+            };
+        }
+        current = current[segments[i]];
+    }
+    return { resolved: true };
+}
+function editDistance(left, right) {
+    const previous = Array.from({ length: right.length + 1 }, (_value, i) => i);
+    for (let i = 1; i <= left.length; i += 1) {
+        let diagonal = previous[0];
+        previous[0] = i;
+        for (let j = 1; j <= right.length; j += 1) {
+            const candidate = Math.min(previous[j] + 1, previous[j - 1] + 1, diagonal + (left[i - 1] === right[j - 1] ? 0 : 1));
+            diagonal = previous[j];
+            previous[j] = candidate;
+        }
+    }
+    return previous[right.length];
+}
+/** Nearest candidate, when one is near enough that offering it is help rather than noise. */
+export function suggestName(name, candidates) {
+    let best;
+    candidates.forEach((candidate) => {
+        const distance = editDistance(name.toLowerCase(), candidate.toLowerCase());
+        if (!best || distance < best.distance)
+            best = { name: candidate, distance };
+    });
+    if (!best)
+        return undefined;
+    const limit = name.length <= 4 ? 1 : 2;
+    return best.distance > 0 && best.distance <= limit ? best.name : undefined;
+}
+/**
+ * Expressions in `template` that will not resolve against `context`.
+ *
+ * Only `{{ … }}` output is checked, never `{% if … %}` conditions: testing whether an optional
+ * value is present is idiomatic — the templates here open with
+ * `{% if subworkflowContext.MATERIAL_INDEX %}` — while an unresolved `{{ … }}` silently writes an
+ * empty string into the file the simulation runs on.
+ */
+export function findUnresolvedVariables(template, context) {
+    if (!template || !context)
+        return [];
+    const scanned = blankRawBlocks(template);
+    const localNames = collectLocalNames(scanned);
+    const issues = [];
+    const seen = new Set();
+    extractOutputExpressions(scanned).forEach(({ expression, index }) => {
+        const line = scanned.slice(0, index).split("\n").length;
+        extractPaths(expression).forEach((path) => {
+            var _a, _b;
+            const [root] = path.split(".");
+            if (localNames.has(root))
+                return;
+            const resolution = resolvePath(context, path);
+            if (resolution.resolved || !resolution.missing)
+                return;
+            const key = `${line}:${resolution.missing}`;
+            if (seen.has(key))
+                return;
+            seen.add(key);
+            const missingSegment = (_a = resolution.missing.split(".").pop()) !== null && _a !== void 0 ? _a : resolution.missing;
+            const suggestedSegment = suggestName(missingSegment, (_b = resolution.available) !== null && _b !== void 0 ? _b : []);
+            const prefix = resolution.missing.split(".").slice(0, -1).join(".");
+            issues.push({
+                line,
+                expression: expression.trim(),
+                name: resolution.missing,
+                ...(suggestedSegment
+                    ? { suggestion: prefix ? `${prefix}.${suggestedSegment}` : suggestedSegment }
+                    : {}),
+            });
+        });
+    });
+    return issues;
+}
diff --git a/src/components/ExecutionUnit.tsx b/src/components/ExecutionUnit.tsx
index 40ae35a..9b516a5 100644
--- a/src/components/ExecutionUnit.tsx
+++ b/src/components/ExecutionUnit.tsx
@@ -13,13 +13,14 @@ import List from "@mui/material/List";
 import ListItem from "@mui/material/ListItem";
 import Radio from "@mui/material/Radio";
 import Stack from "@mui/material/Stack";
-import React, { useCallback, useEffect, useState } from "react";
+import React, { useCallback, useEffect, useMemo, useState } from "react";
 
 import { Application } from "./Application";
 import {
     type ExecutionUnitInput,
     ExecutionUnitInputFilePanel,
 } from "./ExecutionUnitInputFilePanel";
+import { TemplateVariablesPanel } from "./TemplateVariablesPanel";
 import {
     DrawerContainer,
     DrawerContent,
@@ -30,6 +31,7 @@ import {
 } from "./SideDrawer";
 
 import TabsMenu from "@mat3ra/cove/dist/mui/components/tabs/TabsMenu";
+import { findUnresolvedVariables } from "../utils/templateVariables";
 import type { ExecutableSchema, FlavorSchema } from "@mat3ra/esse/dist/js/types";
 
 
@@ -51,6 +53,8 @@ export type ExecutionUnitProps = {
         availableUnits: AnySubworkflowUnit[];
         onChange: (value: string) => void;
     }>;
+    /** Labels for top-level rendering-context keys, when the host knows better than the default. */
+    variableOriginOverrides?: Record;
     /** Injected component for unit details (results, monitors, post-processors). */
     UnitDetailsComponent?: React.ComponentType<{
         unit: ExecutionUnitSchema;
@@ -81,6 +85,7 @@ export function ExecutionUnit({
     units = [],
     UnitPointerFieldComponent,
     UnitDetailsComponent,
+    variableOriginOverrides,
 }: ExecutionUnitProps) {
     const [drawerContent, setDrawerContent] = useState<"context" | "materialList">("context");
     const [isDrawerVisible, setIsDrawerVisible] = useState(false);
@@ -261,6 +266,19 @@ export function ExecutionUnit({
         [unit, onUpdate],
     );
 
+    /**
+     * nunjucks substitutes an unknown variable with the empty string, so a typo does not fail —
+     * it quietly drops a value out of the input file. Checked per input so the warning can sit
+     * with the template it belongs to.
+     */
+    const issuesByInput = useMemo(
+        () =>
+            unit.input.map((inputRow: ExecutionUnitInput) =>
+                findUnresolvedVariables(inputRow.template?.content, renderingContext),
+            ),
+        [unit.input, renderingContext],
+    );
+
     const tabs = unit.input.map((inputRow: ExecutionUnitInput, index: number) => {
         const label = inputRow.template.name;
         return {
@@ -339,6 +357,7 @@ export function ExecutionUnit({
                                 lineWrapping={lineWrapping}
                                 adjustable={adjustable}
                                 isStandalone={isStandalone}
+                                issues={issuesByInput[index]}
                             />
                         ))}
                     
@@ -346,36 +365,32 @@ export function ExecutionUnit({
                         
                              setIsDrawerVisible((v) => !v)}
                                 icon={isDrawerVisible ? "shapes.arrow.right" : "shapes.arrow.left"}
                             />
                             
                              setDrawerContent("context")}
                                 icon="pages.context"
                             />
                              setDrawerContent("materialList")}
                                 icon="entities.material"
                             />
                         
                         
+                            title={drawerContent === "context" ? "Variables" : "Material"}>
                             {drawerContent === "context" && (
-                                
-                                    {JSON.stringify(renderingContext ?? {}, null, "    ")}
-                                
+ )} {drawerContent === "materialList" && ( diff --git a/src/components/ExecutionUnitInputFilePanel.tsx b/src/components/ExecutionUnitInputFilePanel.tsx index f62b703..9afa35a 100644 --- a/src/components/ExecutionUnitInputFilePanel.tsx +++ b/src/components/ExecutionUnitInputFilePanel.tsx @@ -1,4 +1,5 @@ import CodeMirror from "@mat3ra/cove/dist/other/codemirror"; +import Alert from "@mui/material/Alert"; import { getProgrammingLanguageFromFileExtension } from "@mat3ra/code/dist/js/utils"; import type { ExecutionUnitInputItemSchema } from "@mat3ra/esse/dist/js/types"; import Stack from "@mui/material/Stack"; @@ -6,6 +7,7 @@ import React from "react"; import TabsMenu from "@mat3ra/cove/dist/mui/components/tabs/TabsMenu"; import type { TabItem } from "@mat3ra/cove/dist/mui/components/tabs/types"; +import type { TemplateIssue } from "../utils/templateVariables"; /** `input[]` row for an execution unit; optional `name` is set by `setInputItemNameByIndex` for tab labels. */ export type ExecutionUnitInput = ExecutionUnitInputItemSchema & { @@ -26,6 +28,8 @@ type ExecutionUnitInputFilePanelProps = { lineWrapping: boolean; adjustable?: boolean; isStandalone?: boolean; + /** Variables in this template that will render to nothing. */ + issues?: TemplateIssue[]; }; const codeMirrorDefaults: Record = { @@ -50,6 +54,7 @@ export function ExecutionUnitInputFilePanel({ lineWrapping, adjustable, isStandalone, + issues = [], }: ExecutionUnitInputFilePanelProps) { const contentTabIdString = `template-${index}`; const previewTabIdString = `preview-${index}`; @@ -84,6 +89,23 @@ export function ExecutionUnitInputFilePanel({ id={String(index)} className={`ExecutionFile ${isActive ? "active" : ""}`}> + {/* + Named here rather than left to the preview: nunjucks renders an unknown variable + as the empty string, so the preview of a typo looks like a value that is simply + blank. + */} + {issues.length > 0 && ( + + {issues + .map( + (issue) => + `line ${issue.line}: ${issue.name}${ + issue.suggestion ? ` — did you mean ${issue.suggestion}?` : "" + }`, + ) + .join(" · ")} + + )} ; + /** Host-supplied labels for top-level context keys; see `describeOrigin`. */ + originOverrides?: Record; +} + +/** "renders with: Si … · espresso", from the context itself rather than a separate prop. */ +function describeRenderTarget(context?: Record): string { + const read = (key: string) => { + const value = (context?.[key] ?? {}) as { name?: unknown }; + return typeof value?.name === "string" ? value.name : undefined; + }; + return [read("material"), read("application")].filter(Boolean).join(" · "); +} + +function groupByOrigin(variables: ContextVariable[]): [string, ContextVariable[]][] { + const groups = new Map(); + variables.forEach((variable) => { + const bucket = groups.get(variable.origin); + if (bucket) bucket.push(variable); + else groups.set(variable.origin, [variable]); + }); + return [...groups.entries()]; +} + +/** + * What a template can write, and where each value came from. + * + * Replaces a `
` of the whole rendering context as JSON — everything available, in the shape
+ * it happened to be stored in, with no indication of which parts came from the Settings tab and
+ * which arrive at job runtime. Clicking a row copies the `{{ … }}` expression: inserting at the
+ * cursor needs the editor's `EditorView`, which cove's CodeMirror wrapper does not expose.
+ *
+ * Unresolved variables are reported by {@link ExecutionUnitInputFilePanel}, directly above the
+ * template they are in, rather than here as well.
+ */
+export function TemplateVariablesPanel({
+    renderingContext,
+    originOverrides,
+}: TemplateVariablesPanelProps) {
+    const [search, setSearch] = useState("");
+    const [copied, setCopied] = useState(null);
+
+    const variables = useMemo(
+        () => flattenRenderingContext(renderingContext, { originOverrides }),
+        [renderingContext, originOverrides],
+    );
+
+    const needle = search.trim().toLowerCase();
+    const visible = needle
+        ? variables.filter(
+              (variable) =>
+                  variable.path.toLowerCase().includes(needle) ||
+                  variable.preview.toLowerCase().includes(needle),
+          )
+        : variables;
+
+    const copy = (path: string) => {
+        const expression = `{{ ${path} }}`;
+        navigator.clipboard?.writeText(expression).catch(() => undefined);
+        setCopied(path);
+        // Fire-and-forget; the panel only needs the label to settle back.
+        setTimeout(() => setCopied((current) => (current === path ? null : current)), 1200);
+    };
+
+    const renderTarget = describeRenderTarget(renderingContext);
+
+    return (
+        
+            {renderTarget ? (
+                
+                    renders with: {renderTarget}
+                
+            ) : null}
+
+             setSearch(event.target.value)}
+                placeholder="Search variables"
+                InputProps={{
+                    startAdornment: ,
+                    inputProps: { "data-tid": "template-variables-search" },
+                }}
+            />
+
+            {visible.length === 0 ? (
+                
+                    {variables.length === 0
+                        ? "No context to render this template with yet."
+                        : `Nothing matches “${search}”.`}
+                
+            ) : (
+                groupByOrigin(visible).map(([origin, rows]) => (
+                    
+                        
+                            {origin}
+                        
+                        {rows.map((variable) => (
+                            
+                                 copy(variable.path)}
+                                    data-tid={`template-variable-${variable.path}`}
+                                    sx={{
+                                        display: "flex",
+                                        gap: 1,
+                                        alignItems: "baseline",
+                                        px: 0.75,
+                                        py: 0.25,
+                                        borderRadius: 0.5,
+                                        cursor: "pointer",
+                                        "&:hover": { backgroundColor: "action.hover" },
+                                    }}
+                                >
+                                    
+                                        {variable.path}
+                                    
+                                    
+                                        {variable.preview}
+                                    
+                                
+                            
+                        ))}
+                    
+                ))
+            )}
+        
+    );
+}
+
+export default TemplateVariablesPanel;
diff --git a/src/utils/templateVariables.ts b/src/utils/templateVariables.ts
new file mode 100644
index 0000000..3d9f48b
--- /dev/null
+++ b/src/utils/templateVariables.ts
@@ -0,0 +1,382 @@
+/**
+ * Reading an input template against the context it will render with.
+ *
+ * Two questions the editor could not answer before: what is available to write into a template,
+ * and whether what is written actually resolves. The second matters more than it looks —
+ * templates render through nunjucks, which substitutes an unknown variable with the **empty
+ * string**. A typo does not fail loudly, or even leave `{{ }}` behind in the preview; it quietly
+ * drops a value out of the input file a simulation then runs with.
+ *
+ * Kept free of React so the parsing rules are testable on their own.
+ */
+
+/** One entry of the rendering context, as the variables panel lists it. */
+export interface ContextVariable {
+    /** Dotted path to write into a template, e.g. `cutoffs.wavefunction`. */
+    path: string;
+    /** Top-level key the path hangs off, which is what determines its origin. */
+    root: string;
+    origin: string;
+    /** Short rendering of the current value. */
+    preview: string;
+    /** False for objects and arrays, which are containers rather than values. */
+    isLeaf: boolean;
+}
+
+/** A `{{ … }}` expression that will render to nothing. */
+export interface TemplateIssue {
+    /** 1-based, so it matches the editor's gutter. */
+    line: number;
+    /** The whole expression, for showing what was written. */
+    expression: string;
+    /** The longest prefix of the path that does not resolve. */
+    name: string;
+    /** Nearest available name, when one is close enough to be worth offering. */
+    suggestion?: string;
+}
+
+/**
+ * Where a top-level context key comes from.
+ *
+ * wode builds the rendering context as `{ ...providerName → providerData, ...externalContext }`
+ * (`ExecutionUnit.saveRenderingContext`), so anything **not** in this table arrived from a
+ * context provider — which is what the Settings tab edits. That is why the fallback below is a
+ * statement rather than a guess.
+ */
+const EXTERNAL_CONTEXT_ORIGINS: Record = {
+    material: "From the material",
+    materials: "From the material",
+    materialsSet: "From the material",
+    input: "Derived from the material",
+    application: "From the application",
+    methodData: "From the method",
+    subworkflowContext: "Set at job runtime",
+    workflowHasRelaxation: "From the workflow",
+    jobHasParent: "From the workflow",
+};
+
+const PROVIDER_ORIGIN = "Important settings";
+
+export function describeOrigin(root: string, overrides?: Record): string {
+    return overrides?.[root] ?? EXTERNAL_CONTEXT_ORIGINS[root] ?? PROVIDER_ORIGIN;
+}
+
+/** Entity instances (Material, Application…) are opaque here: only plain data is walked. */
+function isPlainObject(value: unknown): value is Record {
+    if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
+    const prototype = Object.getPrototypeOf(value);
+    return prototype === Object.prototype || prototype === null;
+}
+
+function previewValue(value: unknown): string {
+    if (value === null) return "null";
+    if (value === undefined) return "undefined";
+    if (Array.isArray(value)) return `[${value.length} item${value.length === 1 ? "" : "s"}]`;
+    if (typeof value === "object") {
+        const keys = Object.keys(value as Record);
+        return keys.length
+            ? `{ ${keys.slice(0, 3).join(", ")}${keys.length > 3 ? ", …" : ""} }`
+            : "{ }";
+    }
+    const text = String(value);
+    return text.length > 60 ? `${text.slice(0, 57)}…` : text;
+}
+
+export interface FlattenOptions {
+    /** How far to walk into nested plain objects. Deeper paths are still writable by hand. */
+    maxDepth?: number;
+    /** Backstop for a context that turns out to be far larger than expected. */
+    maxVariables?: number;
+    originOverrides?: Record;
+}
+
+/** Every path a template can write, in the order the context declares them. */
+export function flattenRenderingContext(
+    context: Record | undefined,
+    { maxDepth = 3, maxVariables = 400, originOverrides }: FlattenOptions = {},
+): ContextVariable[] {
+    if (!context || typeof context !== "object") return [];
+    const variables: ContextVariable[] = [];
+
+    const walk = (value: unknown, path: string, root: string, depth: number) => {
+        if (variables.length >= maxVariables) return;
+        const isLeaf = !isPlainObject(value) && !Array.isArray(value);
+        variables.push({
+            path,
+            root,
+            origin: describeOrigin(root, originOverrides),
+            preview: previewValue(value),
+            isLeaf,
+        });
+        if (isPlainObject(value) && depth < maxDepth) {
+            Object.keys(value).forEach((key) =>
+                walk(value[key], `${path}.${key}`, root, depth + 1),
+            );
+        }
+    };
+
+    Object.keys(context).forEach((key) => walk(context[key], key, key, 1));
+    return variables;
+}
+
+// ---------------------------------------------------------------------------
+// Template analysis
+// ---------------------------------------------------------------------------
+
+/** Names nunjucks provides itself, plus the operators that read like identifiers. */
+const RESERVED = new Set([
+    "loop",
+    "range",
+    "cycler",
+    "joiner",
+    "super",
+    "self",
+    "true",
+    "false",
+    "none",
+    "null",
+    "True",
+    "False",
+    "None",
+    "if",
+    "else",
+    "elif",
+    "not",
+    "and",
+    "or",
+    "in",
+    "is",
+    "by",
+]);
+
+/**
+ * Blanks out `{% raw %}` bodies, preserving offsets and line breaks.
+ *
+ * What is inside them is deliberately *not* rendered — job-runtime placeholders such as
+ * `{{ JOB_WORK_DIR }}` — so checking them would report the template's whole point as a defect.
+ */
+function blankRawBlocks(template: string): string {
+    return template.replace(/\{%-?\s*raw\s*-?%\}[\s\S]*?\{%-?\s*endraw\s*-?%\}/g, (block) =>
+        block.replace(/[^\n]/g, " "),
+    );
+}
+
+/** Names the template binds itself, which are not expected to be in the context. */
+export function collectLocalNames(template: string): Set {
+    const names = new Set();
+    const add = (name: string) => {
+        const trimmed = name.trim();
+        if (trimmed) names.add(trimmed);
+    };
+
+    Array.from(template.matchAll(/\{%-?\s*(?:set|with)\s+([A-Za-z_]\w*)/g)).forEach((match) =>
+        add(match[1]),
+    );
+    Array.from(template.matchAll(/\{%-?\s*macro\s+([A-Za-z_]\w*)/g)).forEach((match) =>
+        add(match[1]),
+    );
+    // `{% for a, b in items %}` binds both names.
+    Array.from(template.matchAll(/\{%-?\s*for\s+([\s\S]*?)\s+in\s/g)).forEach((match) =>
+        match[1].split(",").forEach(add),
+    );
+    return names;
+}
+
+/** The `{{ … }}` expressions, with the offset each starts at. */
+function extractOutputExpressions(template: string): { expression: string; index: number }[] {
+    return Array.from(template.matchAll(/\{\{([\s\S]*?)\}\}/g)).map((match) => ({
+        expression: match[1],
+        index: match.index ?? 0,
+    }));
+}
+
+/**
+ * Root-anchored dotted paths referenced by an expression.
+ *
+ * Skips string literals, filter names after `|`, property names after `.` whose root was already
+ * skipped, and anything called as a function. Stops a path at `[`, so a dynamic index such as
+ * `input.perMaterial[subworkflowContext.MATERIAL_INDEX]` contributes `input.perMaterial` and the
+ * index expression separately, rather than one path that could never be resolved.
+ */
+export function extractPaths(expression: string): string[] {
+    const paths: string[] = [];
+    let index = 0;
+    let previous = "";
+
+    while (index < expression.length) {
+        const char = expression[index];
+
+        if (char === "'" || char === '"') {
+            const quote = char;
+            index += 1;
+            while (index < expression.length && expression[index] !== quote) {
+                index += expression[index] === "\\" ? 2 : 1;
+            }
+            index += 1;
+            previous = "literal";
+            continue;
+        }
+
+        if (/\s/.test(char)) {
+            index += 1;
+            continue;
+        }
+
+        if (!/[A-Za-z_]/.test(char)) {
+            previous = char;
+            index += 1;
+            continue;
+        }
+
+        let end = index;
+        while (end < expression.length && /\w/.test(expression[end])) end += 1;
+        const identifier = expression.slice(index, end);
+
+        let afterIdentifier = end;
+        while (afterIdentifier < expression.length && /\s/.test(expression[afterIdentifier])) {
+            afterIdentifier += 1;
+        }
+        const isCall = expression[afterIdentifier] === "(";
+        const isSkipped =
+            previous === "|" || previous === "." || isCall || RESERVED.has(identifier);
+
+        if (isSkipped) {
+            index = end;
+            previous = "identifier";
+            continue;
+        }
+
+        let path = identifier;
+        let cursor = end;
+        while (expression[cursor] === ".") {
+            let segmentEnd = cursor + 1;
+            while (segmentEnd < expression.length && /\w/.test(expression[segmentEnd])) {
+                segmentEnd += 1;
+            }
+            if (segmentEnd === cursor + 1) break;
+            let afterSegment = segmentEnd;
+            while (afterSegment < expression.length && /\s/.test(expression[afterSegment])) {
+                afterSegment += 1;
+            }
+            // A method call is not a context path.
+            if (expression[afterSegment] === "(") break;
+            path += `.${expression.slice(cursor + 1, segmentEnd)}`;
+            cursor = segmentEnd;
+        }
+
+        paths.push(path);
+        index = cursor;
+        previous = "identifier";
+    }
+
+    return paths;
+}
+
+interface Resolution {
+    resolved: boolean;
+    /** Longest prefix that failed, when it did. */
+    missing?: string;
+    /** Keys available where it failed, for suggesting a near match. */
+    available?: string[];
+}
+
+/**
+ * Walks a dotted path through the context, stopping the moment it reaches something that is not
+ * plain data — an entity instance's own properties cannot be enumerated reliably, and reporting
+ * `material.name` as missing because `Material` is a class would be worse than saying nothing.
+ */
+export function resolvePath(context: Record, path: string): Resolution {
+    const segments = path.split(".");
+    let current: unknown = context;
+
+    for (let i = 0; i < segments.length; i += 1) {
+        if (!isPlainObject(current)) {
+            return { resolved: true };
+        }
+        if (!(segments[i] in current)) {
+            return {
+                resolved: false,
+                missing: segments.slice(0, i + 1).join("."),
+                available: Object.keys(current),
+            };
+        }
+        current = current[segments[i]];
+    }
+    return { resolved: true };
+}
+
+function editDistance(left: string, right: string): number {
+    const previous = Array.from({ length: right.length + 1 }, (_value, i) => i);
+    for (let i = 1; i <= left.length; i += 1) {
+        let diagonal = previous[0];
+        previous[0] = i;
+        for (let j = 1; j <= right.length; j += 1) {
+            const candidate = Math.min(
+                previous[j] + 1,
+                previous[j - 1] + 1,
+                diagonal + (left[i - 1] === right[j - 1] ? 0 : 1),
+            );
+            diagonal = previous[j];
+            previous[j] = candidate;
+        }
+    }
+    return previous[right.length];
+}
+
+/** Nearest candidate, when one is near enough that offering it is help rather than noise. */
+export function suggestName(name: string, candidates: string[]): string | undefined {
+    let best: { name: string; distance: number } | undefined;
+    candidates.forEach((candidate) => {
+        const distance = editDistance(name.toLowerCase(), candidate.toLowerCase());
+        if (!best || distance < best.distance) best = { name: candidate, distance };
+    });
+    if (!best) return undefined;
+    const limit = name.length <= 4 ? 1 : 2;
+    return best.distance > 0 && best.distance <= limit ? best.name : undefined;
+}
+
+/**
+ * Expressions in `template` that will not resolve against `context`.
+ *
+ * Only `{{ … }}` output is checked, never `{% if … %}` conditions: testing whether an optional
+ * value is present is idiomatic — the templates here open with
+ * `{% if subworkflowContext.MATERIAL_INDEX %}` — while an unresolved `{{ … }}` silently writes an
+ * empty string into the file the simulation runs on.
+ */
+export function findUnresolvedVariables(
+    template: string | undefined,
+    context: Record | undefined,
+): TemplateIssue[] {
+    if (!template || !context) return [];
+
+    const scanned = blankRawBlocks(template);
+    const localNames = collectLocalNames(scanned);
+    const issues: TemplateIssue[] = [];
+    const seen = new Set();
+
+    extractOutputExpressions(scanned).forEach(({ expression, index }) => {
+        const line = scanned.slice(0, index).split("\n").length;
+        extractPaths(expression).forEach((path) => {
+            const [root] = path.split(".");
+            if (localNames.has(root)) return;
+            const resolution = resolvePath(context, path);
+            if (resolution.resolved || !resolution.missing) return;
+            const key = `${line}:${resolution.missing}`;
+            if (seen.has(key)) return;
+            seen.add(key);
+            const missingSegment = resolution.missing.split(".").pop() ?? resolution.missing;
+            const suggestedSegment = suggestName(missingSegment, resolution.available ?? []);
+            const prefix = resolution.missing.split(".").slice(0, -1).join(".");
+            issues.push({
+                line,
+                expression: expression.trim(),
+                name: resolution.missing,
+                ...(suggestedSegment
+                    ? { suggestion: prefix ? `${prefix}.${suggestedSegment}` : suggestedSegment }
+                    : {}),
+            });
+        });
+    });
+
+    return issues;
+}
diff --git a/tests/templateVariables.tests.ts b/tests/templateVariables.tests.ts
new file mode 100644
index 0000000..7ecf532
--- /dev/null
+++ b/tests/templateVariables.tests.ts
@@ -0,0 +1,184 @@
+/* eslint-disable @typescript-eslint/no-floating-promises */
+import assert from "node:assert";
+import test from "node:test";
+
+import {
+    collectLocalNames,
+    describeOrigin,
+    extractPaths,
+    findUnresolvedVariables,
+    flattenRenderingContext,
+    resolvePath,
+    suggestName,
+} from "../src/utils/templateVariables";
+
+/** Shaped like a real execution unit's context: provider data, then the external context. */
+const CONTEXT = {
+    cutoffs: { wavefunction: 40, density: 200 },
+    kgrid: { dimensions: [2, 2, 2], shifts: [0, 0, 0] },
+    input: { IBRAV: 0, NAT: 2, RESTART_MODE: "from_scratch" },
+    material: Object.assign(Object.create({ name: "Si" }), { _json: {} }),
+    materials: [{}],
+    application: { name: "espresso", version: "6.3" },
+    subworkflowContext: {},
+};
+
+test("origins name where a variable came from", () => {
+    assert.strictEqual(describeOrigin("material"), "From the material");
+    assert.strictEqual(describeOrigin("subworkflowContext"), "Set at job runtime");
+    // Not in the external-context table, so wode put it there from a context provider.
+    assert.strictEqual(describeOrigin("cutoffs"), "Important settings");
+    assert.strictEqual(describeOrigin("kgrid"), "Important settings");
+    assert.strictEqual(describeOrigin("cutoffs", { cutoffs: "Custom" }), "Custom");
+    // The external context carries more than the material: these came through the subworkflow,
+    // not through a provider, and mislabelling them sends people to the wrong tab.
+    assert.strictEqual(describeOrigin("materialsSet"), "From the material");
+    assert.strictEqual(describeOrigin("jobHasParent"), "From the workflow");
+});
+
+test("the context flattens to writable dotted paths", () => {
+    const variables = flattenRenderingContext(CONTEXT);
+    const paths = variables.map((variable) => variable.path);
+    assert.ok(paths.includes("cutoffs"));
+    assert.ok(paths.includes("cutoffs.wavefunction"));
+    assert.ok(paths.includes("input.RESTART_MODE"));
+    const wavefunction = variables.find((variable) => variable.path === "cutoffs.wavefunction");
+    assert.strictEqual(wavefunction?.preview, "40");
+    assert.strictEqual(wavefunction?.origin, "Important settings");
+    assert.strictEqual(wavefunction?.isLeaf, true);
+});
+
+test("containers are listed but marked, and arrays report their size", () => {
+    const variables = flattenRenderingContext(CONTEXT);
+    const cutoffs = variables.find((variable) => variable.path === "cutoffs");
+    assert.strictEqual(cutoffs?.isLeaf, false);
+    const dimensions = variables.find((variable) => variable.path === "kgrid.dimensions");
+    assert.strictEqual(dimensions?.preview, "[3 items]");
+});
+
+test("entity instances are one row, not walked", () => {
+    const paths = flattenRenderingContext(CONTEXT).map((variable) => variable.path);
+    assert.ok(paths.includes("material"));
+    assert.ok(!paths.some((path) => path.startsWith("material.")));
+});
+
+test("flattening is bounded by depth and count", () => {
+    const deep = { a: { b: { c: { d: { e: 1 } } } } };
+    const paths = flattenRenderingContext(deep, { maxDepth: 2 }).map((v) => v.path);
+    assert.deepStrictEqual(paths, ["a", "a.b"]);
+    const wide = Object.fromEntries(Array.from({ length: 50 }, (_v, i) => [`k${i}`, i]));
+    assert.strictEqual(flattenRenderingContext(wide, { maxVariables: 10 }).length, 10);
+});
+
+test("an absent context yields nothing rather than throwing", () => {
+    assert.deepStrictEqual(flattenRenderingContext(undefined), []);
+});
+
+test("paths are extracted from an expression, skipping literals and filters", () => {
+    assert.deepStrictEqual(extractPaths(" input.RESTART_MODE "), ["input.RESTART_MODE"]);
+    assert.deepStrictEqual(extractPaths(" cutoffs.wavefunction | round "), [
+        "cutoffs.wavefunction",
+    ]);
+    assert.deepStrictEqual(extractPaths(' "a literal" '), []);
+    assert.deepStrictEqual(extractPaths(" a + b "), ["a", "b"]);
+});
+
+test("a dynamic index contributes the container and the index separately", () => {
+    assert.deepStrictEqual(extractPaths("input.perMaterial[subworkflowContext.MATERIAL_INDEX]"), [
+        "input.perMaterial",
+        "subworkflowContext.MATERIAL_INDEX",
+    ]);
+});
+
+test("a called name is not a context path, but the receiver still is", () => {
+    assert.deepStrictEqual(extractPaths(" range(3) "), []);
+    // `material` has to resolve for the call to work; `getName` is a method, not context data.
+    assert.deepStrictEqual(extractPaths(" material.getName() "), ["material"]);
+    assert.deepStrictEqual(extractPaths(" loop.index "), []);
+});
+
+test("names the template binds itself are collected", () => {
+    const names = collectLocalNames(
+        "{%- set input = input.perMaterial[0] -%}{% for name, value in items %}{% macro row(x) %}",
+    );
+    assert.ok(names.has("input"));
+    assert.ok(names.has("name"));
+    assert.ok(names.has("value"));
+    assert.ok(names.has("row"));
+});
+
+test("resolution walks plain data and gives up at an entity instance", () => {
+    assert.strictEqual(resolvePath(CONTEXT, "cutoffs.wavefunction").resolved, true);
+    // `material` is a class instance: unverifiable, so not reported as missing.
+    assert.strictEqual(resolvePath(CONTEXT, "material.name").resolved, true);
+    const missing = resolvePath(CONTEXT, "cutoffs.wavefunctionn");
+    assert.strictEqual(missing.resolved, false);
+    assert.strictEqual(missing.missing, "cutoffs.wavefunctionn");
+    assert.deepStrictEqual(missing.available, ["wavefunction", "density"]);
+});
+
+test("suggestions are offered only when a name is genuinely close", () => {
+    assert.strictEqual(suggestName("wavefunctionn", ["wavefunction", "density"]), "wavefunction");
+    assert.strictEqual(suggestName("cutofs", ["cutoffs", "kgrid"]), "cutoffs");
+    assert.strictEqual(suggestName("totallyUnrelated", ["cutoffs", "kgrid"]), undefined);
+    // Short names need an exact-ish match, or every three-letter typo suggests every other one.
+    assert.strictEqual(suggestName("abc", ["xyz"]), undefined);
+});
+
+test("a typo'd variable is reported with its line and a suggestion", () => {
+    const template = ["&SYSTEM", "  ecutwfc = {{ cutoffs.wavefunctionn }}", "/"].join("\n");
+    const [issue, ...rest] = findUnresolvedVariables(template, CONTEXT);
+    assert.strictEqual(rest.length, 0);
+    assert.strictEqual(issue.line, 2);
+    assert.strictEqual(issue.name, "cutoffs.wavefunctionn");
+    assert.strictEqual(issue.suggestion, "cutoffs.wavefunction");
+    assert.strictEqual(issue.expression, "cutoffs.wavefunctionn");
+});
+
+test("a valid template reports nothing", () => {
+    const template = [
+        "{% if subworkflowContext.MATERIAL_INDEX %}",
+        "{%- set input = input.perMaterial[subworkflowContext.MATERIAL_INDEX] -%}",
+        "{% endif -%}",
+        "  ecutwfc = {{ cutoffs.wavefunction }}",
+        "  ecutrho = {{ cutoffs.density }}",
+        "  ibrav = {{ input.IBRAV }}",
+        "  nat = {{ input.NAT }}",
+    ].join("\n");
+    assert.deepStrictEqual(findUnresolvedVariables(template, CONTEXT), []);
+});
+
+test("job-runtime placeholders inside raw blocks are left alone", () => {
+    const template = "outdir = {% raw %}'{{ JOB_WORK_DIR }}/outdir'{% endraw %}";
+    assert.deepStrictEqual(findUnresolvedVariables(template, CONTEXT), []);
+});
+
+test("raw blocks do not shift the line numbers of what follows", () => {
+    const template = ["{% raw %}", "{{ JOB_WORK_DIR }}", "{% endraw %}", "{{ nowhere }}"].join(
+        "\n",
+    );
+    const [issue] = findUnresolvedVariables(template, CONTEXT);
+    assert.strictEqual(issue.line, 4);
+    assert.strictEqual(issue.name, "nowhere");
+});
+
+test("an {% if %} test for an absent value is not an issue", () => {
+    const template = "{% if subworkflowContext.NOT_SET %}x{% endif %}";
+    assert.deepStrictEqual(findUnresolvedVariables(template, CONTEXT), []);
+});
+
+test("the same miss on one line is reported once", () => {
+    const template = "{{ nope }} and {{ nope }}";
+    assert.strictEqual(findUnresolvedVariables(template, CONTEXT).length, 1);
+});
+
+test("an unknown root is reported without inventing a suggestion", () => {
+    const [issue] = findUnresolvedVariables("{{ somethingEntirelyElse.x }}", CONTEXT);
+    assert.strictEqual(issue.name, "somethingEntirelyElse");
+    assert.strictEqual(issue.suggestion, undefined);
+});
+
+test("no template or no context means no claims either way", () => {
+    assert.deepStrictEqual(findUnresolvedVariables(undefined, CONTEXT), []);
+    assert.deepStrictEqual(findUnresolvedVariables("{{ nope }}", undefined), []);
+});

From b3188d830726ed66f785b96e1e784f58ecc0decf Mon Sep 17 00:00:00 2001
From: Claude 
Date: Thu, 27 Aug 2026 03:38:51 +0000
Subject: [PATCH 3/4] chore: stop committing dist/ (build output; see
 mat3ra/agents#5)

---
 dist/components/TemplateVariablesPanel.d.ts |  19 --
 dist/components/TemplateVariablesPanel.js   |  81 -----
 dist/utils/templateVariables.d.ts           |  80 -----
 dist/utils/templateVariables.js             | 308 --------------------
 4 files changed, 488 deletions(-)
 delete mode 100644 dist/components/TemplateVariablesPanel.d.ts
 delete mode 100644 dist/components/TemplateVariablesPanel.js
 delete mode 100644 dist/utils/templateVariables.d.ts
 delete mode 100644 dist/utils/templateVariables.js

diff --git a/dist/components/TemplateVariablesPanel.d.ts b/dist/components/TemplateVariablesPanel.d.ts
deleted file mode 100644
index 3202f8a..0000000
--- a/dist/components/TemplateVariablesPanel.d.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import React from "react";
-export interface TemplateVariablesPanelProps {
-    renderingContext?: Record;
-    /** Host-supplied labels for top-level context keys; see `describeOrigin`. */
-    originOverrides?: Record;
-}
-/**
- * What a template can write, and where each value came from.
- *
- * Replaces a `
` of the whole rendering context as JSON — everything available, in the shape
- * it happened to be stored in, with no indication of which parts came from the Settings tab and
- * which arrive at job runtime. Clicking a row copies the `{{ … }}` expression: inserting at the
- * cursor needs the editor's `EditorView`, which cove's CodeMirror wrapper does not expose.
- *
- * Unresolved variables are reported by {@link ExecutionUnitInputFilePanel}, directly above the
- * template they are in, rather than here as well.
- */
-export declare function TemplateVariablesPanel({ renderingContext, originOverrides, }: TemplateVariablesPanelProps): React.JSX.Element;
-export default TemplateVariablesPanel;
diff --git a/dist/components/TemplateVariablesPanel.js b/dist/components/TemplateVariablesPanel.js
deleted file mode 100644
index 7061ee5..0000000
--- a/dist/components/TemplateVariablesPanel.js
+++ /dev/null
@@ -1,81 +0,0 @@
-import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
-import Box from "@mui/material/Box";
-import InputAdornment from "@mui/material/InputAdornment";
-import Stack from "@mui/material/Stack";
-import TextField from "@mui/material/TextField";
-import Tooltip from "@mui/material/Tooltip";
-import Typography from "@mui/material/Typography";
-import { useMemo, useState } from "react";
-import { flattenRenderingContext } from "../utils/templateVariables";
-/** "renders with: Si … · espresso", from the context itself rather than a separate prop. */
-function describeRenderTarget(context) {
-    const read = (key) => {
-        var _a;
-        const value = ((_a = context === null || context === void 0 ? void 0 : context[key]) !== null && _a !== void 0 ? _a : {});
-        return typeof (value === null || value === void 0 ? void 0 : value.name) === "string" ? value.name : undefined;
-    };
-    return [read("material"), read("application")].filter(Boolean).join(" · ");
-}
-function groupByOrigin(variables) {
-    const groups = new Map();
-    variables.forEach((variable) => {
-        const bucket = groups.get(variable.origin);
-        if (bucket)
-            bucket.push(variable);
-        else
-            groups.set(variable.origin, [variable]);
-    });
-    return [...groups.entries()];
-}
-/**
- * What a template can write, and where each value came from.
- *
- * Replaces a `
` of the whole rendering context as JSON — everything available, in the shape
- * it happened to be stored in, with no indication of which parts came from the Settings tab and
- * which arrive at job runtime. Clicking a row copies the `{{ … }}` expression: inserting at the
- * cursor needs the editor's `EditorView`, which cove's CodeMirror wrapper does not expose.
- *
- * Unresolved variables are reported by {@link ExecutionUnitInputFilePanel}, directly above the
- * template they are in, rather than here as well.
- */
-export function TemplateVariablesPanel({ renderingContext, originOverrides, }) {
-    const [search, setSearch] = useState("");
-    const [copied, setCopied] = useState(null);
-    const variables = useMemo(() => flattenRenderingContext(renderingContext, { originOverrides }), [renderingContext, originOverrides]);
-    const needle = search.trim().toLowerCase();
-    const visible = needle
-        ? variables.filter((variable) => variable.path.toLowerCase().includes(needle) ||
-            variable.preview.toLowerCase().includes(needle))
-        : variables;
-    const copy = (path) => {
-        var _a;
-        const expression = `{{ ${path} }}`;
-        (_a = navigator.clipboard) === null || _a === void 0 ? void 0 : _a.writeText(expression).catch(() => undefined);
-        setCopied(path);
-        // Fire-and-forget; the panel only needs the label to settle back.
-        setTimeout(() => setCopied((current) => (current === path ? null : current)), 1200);
-    };
-    const renderTarget = describeRenderTarget(renderingContext);
-    return (_jsxs(Stack, { spacing: 1.5, sx: { p: 1.5 }, "data-tid": "template-variables-panel", children: [renderTarget ? (_jsxs(Typography, { variant: "caption", color: "text.secondary", children: ["renders with: ", renderTarget] })) : null, _jsx(TextField, { size: "small", value: search, onChange: (event) => setSearch(event.target.value), placeholder: "Search variables", InputProps: {
-                    startAdornment: _jsx(InputAdornment, { position: "start", children: "\u2315" }),
-                    inputProps: { "data-tid": "template-variables-search" },
-                } }), visible.length === 0 ? (_jsx(Typography, { variant: "body2", color: "text.secondary", children: variables.length === 0
-                    ? "No context to render this template with yet."
-                    : `Nothing matches “${search}”.` })) : (groupByOrigin(visible).map(([origin, rows]) => (_jsxs(Box, { children: [_jsx(Typography, { variant: "overline", color: "text.secondary", children: origin }), rows.map((variable) => (_jsx(Tooltip, { title: copied === variable.path
-                            ? "Copied"
-                            : `Copy {{ ${variable.path} }}`, children: _jsxs(Box, { onClick: () => copy(variable.path), "data-tid": `template-variable-${variable.path}`, sx: {
-                                display: "flex",
-                                gap: 1,
-                                alignItems: "baseline",
-                                px: 0.75,
-                                py: 0.25,
-                                borderRadius: 0.5,
-                                cursor: "pointer",
-                                "&:hover": { backgroundColor: "action.hover" },
-                            }, children: [_jsx(Typography, { variant: "caption", sx: {
-                                        fontFamily: "monospace",
-                                        fontWeight: variable.isLeaf ? 400 : 600,
-                                        whiteSpace: "nowrap",
-                                    }, children: variable.path }), _jsx(Typography, { variant: "caption", color: "text.secondary", noWrap: true, sx: { minWidth: 0 }, children: variable.preview })] }) }, variable.path)))] }, origin))))] }));
-}
-export default TemplateVariablesPanel;
diff --git a/dist/utils/templateVariables.d.ts b/dist/utils/templateVariables.d.ts
deleted file mode 100644
index 158bf90..0000000
--- a/dist/utils/templateVariables.d.ts
+++ /dev/null
@@ -1,80 +0,0 @@
-/**
- * Reading an input template against the context it will render with.
- *
- * Two questions the editor could not answer before: what is available to write into a template,
- * and whether what is written actually resolves. The second matters more than it looks —
- * templates render through nunjucks, which substitutes an unknown variable with the **empty
- * string**. A typo does not fail loudly, or even leave `{{ }}` behind in the preview; it quietly
- * drops a value out of the input file a simulation then runs with.
- *
- * Kept free of React so the parsing rules are testable on their own.
- */
-/** One entry of the rendering context, as the variables panel lists it. */
-export interface ContextVariable {
-    /** Dotted path to write into a template, e.g. `cutoffs.wavefunction`. */
-    path: string;
-    /** Top-level key the path hangs off, which is what determines its origin. */
-    root: string;
-    origin: string;
-    /** Short rendering of the current value. */
-    preview: string;
-    /** False for objects and arrays, which are containers rather than values. */
-    isLeaf: boolean;
-}
-/** A `{{ … }}` expression that will render to nothing. */
-export interface TemplateIssue {
-    /** 1-based, so it matches the editor's gutter. */
-    line: number;
-    /** The whole expression, for showing what was written. */
-    expression: string;
-    /** The longest prefix of the path that does not resolve. */
-    name: string;
-    /** Nearest available name, when one is close enough to be worth offering. */
-    suggestion?: string;
-}
-export declare function describeOrigin(root: string, overrides?: Record): string;
-export interface FlattenOptions {
-    /** How far to walk into nested plain objects. Deeper paths are still writable by hand. */
-    maxDepth?: number;
-    /** Backstop for a context that turns out to be far larger than expected. */
-    maxVariables?: number;
-    originOverrides?: Record;
-}
-/** Every path a template can write, in the order the context declares them. */
-export declare function flattenRenderingContext(context: Record | undefined, { maxDepth, maxVariables, originOverrides }?: FlattenOptions): ContextVariable[];
-/** Names the template binds itself, which are not expected to be in the context. */
-export declare function collectLocalNames(template: string): Set;
-/**
- * Root-anchored dotted paths referenced by an expression.
- *
- * Skips string literals, filter names after `|`, property names after `.` whose root was already
- * skipped, and anything called as a function. Stops a path at `[`, so a dynamic index such as
- * `input.perMaterial[subworkflowContext.MATERIAL_INDEX]` contributes `input.perMaterial` and the
- * index expression separately, rather than one path that could never be resolved.
- */
-export declare function extractPaths(expression: string): string[];
-interface Resolution {
-    resolved: boolean;
-    /** Longest prefix that failed, when it did. */
-    missing?: string;
-    /** Keys available where it failed, for suggesting a near match. */
-    available?: string[];
-}
-/**
- * Walks a dotted path through the context, stopping the moment it reaches something that is not
- * plain data — an entity instance's own properties cannot be enumerated reliably, and reporting
- * `material.name` as missing because `Material` is a class would be worse than saying nothing.
- */
-export declare function resolvePath(context: Record, path: string): Resolution;
-/** Nearest candidate, when one is near enough that offering it is help rather than noise. */
-export declare function suggestName(name: string, candidates: string[]): string | undefined;
-/**
- * Expressions in `template` that will not resolve against `context`.
- *
- * Only `{{ … }}` output is checked, never `{% if … %}` conditions: testing whether an optional
- * value is present is idiomatic — the templates here open with
- * `{% if subworkflowContext.MATERIAL_INDEX %}` — while an unresolved `{{ … }}` silently writes an
- * empty string into the file the simulation runs on.
- */
-export declare function findUnresolvedVariables(template: string | undefined, context: Record | undefined): TemplateIssue[];
-export {};
diff --git a/dist/utils/templateVariables.js b/dist/utils/templateVariables.js
deleted file mode 100644
index 2556344..0000000
--- a/dist/utils/templateVariables.js
+++ /dev/null
@@ -1,308 +0,0 @@
-/**
- * Reading an input template against the context it will render with.
- *
- * Two questions the editor could not answer before: what is available to write into a template,
- * and whether what is written actually resolves. The second matters more than it looks —
- * templates render through nunjucks, which substitutes an unknown variable with the **empty
- * string**. A typo does not fail loudly, or even leave `{{ }}` behind in the preview; it quietly
- * drops a value out of the input file a simulation then runs with.
- *
- * Kept free of React so the parsing rules are testable on their own.
- */
-/**
- * Where a top-level context key comes from.
- *
- * wode builds the rendering context as `{ ...providerName → providerData, ...externalContext }`
- * (`ExecutionUnit.saveRenderingContext`), so anything **not** in this table arrived from a
- * context provider — which is what the Settings tab edits. That is why the fallback below is a
- * statement rather than a guess.
- */
-const EXTERNAL_CONTEXT_ORIGINS = {
-    material: "From the material",
-    materials: "From the material",
-    materialsSet: "From the material",
-    input: "Derived from the material",
-    application: "From the application",
-    methodData: "From the method",
-    subworkflowContext: "Set at job runtime",
-    workflowHasRelaxation: "From the workflow",
-    jobHasParent: "From the workflow",
-};
-const PROVIDER_ORIGIN = "Important settings";
-export function describeOrigin(root, overrides) {
-    var _a, _b;
-    return (_b = (_a = overrides === null || overrides === void 0 ? void 0 : overrides[root]) !== null && _a !== void 0 ? _a : EXTERNAL_CONTEXT_ORIGINS[root]) !== null && _b !== void 0 ? _b : PROVIDER_ORIGIN;
-}
-/** Entity instances (Material, Application…) are opaque here: only plain data is walked. */
-function isPlainObject(value) {
-    if (typeof value !== "object" || value === null || Array.isArray(value))
-        return false;
-    const prototype = Object.getPrototypeOf(value);
-    return prototype === Object.prototype || prototype === null;
-}
-function previewValue(value) {
-    if (value === null)
-        return "null";
-    if (value === undefined)
-        return "undefined";
-    if (Array.isArray(value))
-        return `[${value.length} item${value.length === 1 ? "" : "s"}]`;
-    if (typeof value === "object") {
-        const keys = Object.keys(value);
-        return keys.length
-            ? `{ ${keys.slice(0, 3).join(", ")}${keys.length > 3 ? ", …" : ""} }`
-            : "{ }";
-    }
-    const text = String(value);
-    return text.length > 60 ? `${text.slice(0, 57)}…` : text;
-}
-/** Every path a template can write, in the order the context declares them. */
-export function flattenRenderingContext(context, { maxDepth = 3, maxVariables = 400, originOverrides } = {}) {
-    if (!context || typeof context !== "object")
-        return [];
-    const variables = [];
-    const walk = (value, path, root, depth) => {
-        if (variables.length >= maxVariables)
-            return;
-        const isLeaf = !isPlainObject(value) && !Array.isArray(value);
-        variables.push({
-            path,
-            root,
-            origin: describeOrigin(root, originOverrides),
-            preview: previewValue(value),
-            isLeaf,
-        });
-        if (isPlainObject(value) && depth < maxDepth) {
-            Object.keys(value).forEach((key) => walk(value[key], `${path}.${key}`, root, depth + 1));
-        }
-    };
-    Object.keys(context).forEach((key) => walk(context[key], key, key, 1));
-    return variables;
-}
-// ---------------------------------------------------------------------------
-// Template analysis
-// ---------------------------------------------------------------------------
-/** Names nunjucks provides itself, plus the operators that read like identifiers. */
-const RESERVED = new Set([
-    "loop",
-    "range",
-    "cycler",
-    "joiner",
-    "super",
-    "self",
-    "true",
-    "false",
-    "none",
-    "null",
-    "True",
-    "False",
-    "None",
-    "if",
-    "else",
-    "elif",
-    "not",
-    "and",
-    "or",
-    "in",
-    "is",
-    "by",
-]);
-/**
- * Blanks out `{% raw %}` bodies, preserving offsets and line breaks.
- *
- * What is inside them is deliberately *not* rendered — job-runtime placeholders such as
- * `{{ JOB_WORK_DIR }}` — so checking them would report the template's whole point as a defect.
- */
-function blankRawBlocks(template) {
-    return template.replace(/\{%-?\s*raw\s*-?%\}[\s\S]*?\{%-?\s*endraw\s*-?%\}/g, (block) => block.replace(/[^\n]/g, " "));
-}
-/** Names the template binds itself, which are not expected to be in the context. */
-export function collectLocalNames(template) {
-    const names = new Set();
-    const add = (name) => {
-        const trimmed = name.trim();
-        if (trimmed)
-            names.add(trimmed);
-    };
-    Array.from(template.matchAll(/\{%-?\s*(?:set|with)\s+([A-Za-z_]\w*)/g)).forEach((match) => add(match[1]));
-    Array.from(template.matchAll(/\{%-?\s*macro\s+([A-Za-z_]\w*)/g)).forEach((match) => add(match[1]));
-    // `{% for a, b in items %}` binds both names.
-    Array.from(template.matchAll(/\{%-?\s*for\s+([\s\S]*?)\s+in\s/g)).forEach((match) => match[1].split(",").forEach(add));
-    return names;
-}
-/** The `{{ … }}` expressions, with the offset each starts at. */
-function extractOutputExpressions(template) {
-    return Array.from(template.matchAll(/\{\{([\s\S]*?)\}\}/g)).map((match) => {
-        var _a;
-        return ({
-            expression: match[1],
-            index: (_a = match.index) !== null && _a !== void 0 ? _a : 0,
-        });
-    });
-}
-/**
- * Root-anchored dotted paths referenced by an expression.
- *
- * Skips string literals, filter names after `|`, property names after `.` whose root was already
- * skipped, and anything called as a function. Stops a path at `[`, so a dynamic index such as
- * `input.perMaterial[subworkflowContext.MATERIAL_INDEX]` contributes `input.perMaterial` and the
- * index expression separately, rather than one path that could never be resolved.
- */
-export function extractPaths(expression) {
-    const paths = [];
-    let index = 0;
-    let previous = "";
-    while (index < expression.length) {
-        const char = expression[index];
-        if (char === "'" || char === '"') {
-            const quote = char;
-            index += 1;
-            while (index < expression.length && expression[index] !== quote) {
-                index += expression[index] === "\\" ? 2 : 1;
-            }
-            index += 1;
-            previous = "literal";
-            continue;
-        }
-        if (/\s/.test(char)) {
-            index += 1;
-            continue;
-        }
-        if (!/[A-Za-z_]/.test(char)) {
-            previous = char;
-            index += 1;
-            continue;
-        }
-        let end = index;
-        while (end < expression.length && /\w/.test(expression[end]))
-            end += 1;
-        const identifier = expression.slice(index, end);
-        let afterIdentifier = end;
-        while (afterIdentifier < expression.length && /\s/.test(expression[afterIdentifier])) {
-            afterIdentifier += 1;
-        }
-        const isCall = expression[afterIdentifier] === "(";
-        const isSkipped = previous === "|" || previous === "." || isCall || RESERVED.has(identifier);
-        if (isSkipped) {
-            index = end;
-            previous = "identifier";
-            continue;
-        }
-        let path = identifier;
-        let cursor = end;
-        while (expression[cursor] === ".") {
-            let segmentEnd = cursor + 1;
-            while (segmentEnd < expression.length && /\w/.test(expression[segmentEnd])) {
-                segmentEnd += 1;
-            }
-            if (segmentEnd === cursor + 1)
-                break;
-            let afterSegment = segmentEnd;
-            while (afterSegment < expression.length && /\s/.test(expression[afterSegment])) {
-                afterSegment += 1;
-            }
-            // A method call is not a context path.
-            if (expression[afterSegment] === "(")
-                break;
-            path += `.${expression.slice(cursor + 1, segmentEnd)}`;
-            cursor = segmentEnd;
-        }
-        paths.push(path);
-        index = cursor;
-        previous = "identifier";
-    }
-    return paths;
-}
-/**
- * Walks a dotted path through the context, stopping the moment it reaches something that is not
- * plain data — an entity instance's own properties cannot be enumerated reliably, and reporting
- * `material.name` as missing because `Material` is a class would be worse than saying nothing.
- */
-export function resolvePath(context, path) {
-    const segments = path.split(".");
-    let current = context;
-    for (let i = 0; i < segments.length; i += 1) {
-        if (!isPlainObject(current)) {
-            return { resolved: true };
-        }
-        if (!(segments[i] in current)) {
-            return {
-                resolved: false,
-                missing: segments.slice(0, i + 1).join("."),
-                available: Object.keys(current),
-            };
-        }
-        current = current[segments[i]];
-    }
-    return { resolved: true };
-}
-function editDistance(left, right) {
-    const previous = Array.from({ length: right.length + 1 }, (_value, i) => i);
-    for (let i = 1; i <= left.length; i += 1) {
-        let diagonal = previous[0];
-        previous[0] = i;
-        for (let j = 1; j <= right.length; j += 1) {
-            const candidate = Math.min(previous[j] + 1, previous[j - 1] + 1, diagonal + (left[i - 1] === right[j - 1] ? 0 : 1));
-            diagonal = previous[j];
-            previous[j] = candidate;
-        }
-    }
-    return previous[right.length];
-}
-/** Nearest candidate, when one is near enough that offering it is help rather than noise. */
-export function suggestName(name, candidates) {
-    let best;
-    candidates.forEach((candidate) => {
-        const distance = editDistance(name.toLowerCase(), candidate.toLowerCase());
-        if (!best || distance < best.distance)
-            best = { name: candidate, distance };
-    });
-    if (!best)
-        return undefined;
-    const limit = name.length <= 4 ? 1 : 2;
-    return best.distance > 0 && best.distance <= limit ? best.name : undefined;
-}
-/**
- * Expressions in `template` that will not resolve against `context`.
- *
- * Only `{{ … }}` output is checked, never `{% if … %}` conditions: testing whether an optional
- * value is present is idiomatic — the templates here open with
- * `{% if subworkflowContext.MATERIAL_INDEX %}` — while an unresolved `{{ … }}` silently writes an
- * empty string into the file the simulation runs on.
- */
-export function findUnresolvedVariables(template, context) {
-    if (!template || !context)
-        return [];
-    const scanned = blankRawBlocks(template);
-    const localNames = collectLocalNames(scanned);
-    const issues = [];
-    const seen = new Set();
-    extractOutputExpressions(scanned).forEach(({ expression, index }) => {
-        const line = scanned.slice(0, index).split("\n").length;
-        extractPaths(expression).forEach((path) => {
-            var _a, _b;
-            const [root] = path.split(".");
-            if (localNames.has(root))
-                return;
-            const resolution = resolvePath(context, path);
-            if (resolution.resolved || !resolution.missing)
-                return;
-            const key = `${line}:${resolution.missing}`;
-            if (seen.has(key))
-                return;
-            seen.add(key);
-            const missingSegment = (_a = resolution.missing.split(".").pop()) !== null && _a !== void 0 ? _a : resolution.missing;
-            const suggestedSegment = suggestName(missingSegment, (_b = resolution.available) !== null && _b !== void 0 ? _b : []);
-            const prefix = resolution.missing.split(".").slice(0, -1).join(".");
-            issues.push({
-                line,
-                expression: expression.trim(),
-                name: resolution.missing,
-                ...(suggestedSegment
-                    ? { suggestion: prefix ? `${prefix}.${suggestedSegment}` : suggestedSegment }
-                    : {}),
-            });
-        });
-    });
-    return issues;
-}

From 923e0a24dfc9dca0bfdb90f2a86eed2d5e5fc4b7 Mon Sep 17 00:00:00 2001
From: Claude 
Date: Thu, 27 Aug 2026 03:48:36 +0000
Subject: [PATCH 4/4] fix: install ide, made and mode from the registry, not
 deleted WIP tarballs
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

All three were pinned to `wip-` pre-release tarballs. Those exist to test
a commit before its PR merges (mat3ra/agents#5), and the stale-release cleanup
removed them on 2026-08-24 — every URL now 404s, so `npm install` fails and
takes CI with it. Not specific to this branch: main carries the same three
pins and would fail the same way on its next run.

Repointed to the published versions wove already pins, so the two repos
resolve the same ide, made and mode.

Co-Authored-By: Claude Opus 5 
Claude-Session: https://claude.ai/code/session_01FFFpkKD3zHysJXgAbT2uYV
---
 package-lock.json | 2217 ++++-----------------------------------------
 package.json      |    6 +-
 2 files changed, 156 insertions(+), 2067 deletions(-)

diff --git a/package-lock.json b/package-lock.json
index 72bb3e9..894479c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -19,9 +19,9 @@
                 "@mat3ra/code": "2026.8.18-0",
                 "@mat3ra/cove": "2026.8.19-4",
                 "@mat3ra/esse": "2026.8.18-2",
-                "@mat3ra/ide": "https://github.com/mat3ra/ide/releases/download/wip-88b8881/ide.tgz",
-                "@mat3ra/made": "https://github.com/mat3ra/made/releases/download/wip-b5ab8c9/made.tgz",
-                "@mat3ra/mode": "https://github.com/mat3ra/mode/releases/download/wip-f8543e7/mode.tgz",
+                "@mat3ra/ide": "2026.8.18-0",
+                "@mat3ra/made": "2026.8.18-0",
+                "@mat3ra/mode": "2026.8.18-0",
                 "@mat3ra/prode": "2026.8.18-0",
                 "@mat3ra/standata": "2026.8.18-0",
                 "@mat3ra/tsconfig": "^2024.6.3-0",
@@ -3170,113 +3170,6 @@
                 "node": ">=18"
             }
         },
-        "node_modules/@eslint-community/eslint-utils": {
-            "version": "4.10.1",
-            "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz",
-            "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==",
-            "dev": true,
-            "license": "MIT",
-            "peer": true,
-            "dependencies": {
-                "eslint-visitor-keys": "^3.4.3"
-            },
-            "engines": {
-                "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
-            },
-            "funding": {
-                "url": "https://opencollective.com/eslint"
-            },
-            "peerDependencies": {
-                "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
-            }
-        },
-        "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": {
-            "version": "3.4.3",
-            "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
-            "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
-            "dev": true,
-            "license": "Apache-2.0",
-            "peer": true,
-            "engines": {
-                "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
-            },
-            "funding": {
-                "url": "https://opencollective.com/eslint"
-            }
-        },
-        "node_modules/@eslint-community/regexpp": {
-            "version": "4.12.2",
-            "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
-            "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
-            "dev": true,
-            "license": "MIT",
-            "peer": true,
-            "engines": {
-                "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
-            }
-        },
-        "node_modules/@eslint/eslintrc": {
-            "version": "2.1.4",
-            "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz",
-            "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==",
-            "dev": true,
-            "license": "MIT",
-            "peer": true,
-            "dependencies": {
-                "ajv": "^6.12.4",
-                "debug": "^4.3.2",
-                "espree": "^9.6.0",
-                "globals": "^13.19.0",
-                "ignore": "^5.2.0",
-                "import-fresh": "^3.2.1",
-                "js-yaml": "^4.1.0",
-                "minimatch": "^3.1.2",
-                "strip-json-comments": "^3.1.1"
-            },
-            "engines": {
-                "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
-            },
-            "funding": {
-                "url": "https://opencollective.com/eslint"
-            }
-        },
-        "node_modules/@eslint/eslintrc/node_modules/ajv": {
-            "version": "6.15.0",
-            "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz",
-            "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==",
-            "dev": true,
-            "license": "MIT",
-            "peer": true,
-            "dependencies": {
-                "fast-deep-equal": "^3.1.1",
-                "fast-json-stable-stringify": "^2.0.0",
-                "json-schema-traverse": "^0.4.1",
-                "uri-js": "^4.2.2"
-            },
-            "funding": {
-                "type": "github",
-                "url": "https://github.com/sponsors/epoberezkin"
-            }
-        },
-        "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": {
-            "version": "0.4.1",
-            "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
-            "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
-            "dev": true,
-            "license": "MIT",
-            "peer": true
-        },
-        "node_modules/@eslint/js": {
-            "version": "8.57.1",
-            "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz",
-            "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==",
-            "dev": true,
-            "license": "MIT",
-            "peer": true,
-            "engines": {
-                "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
-            }
-        },
         "node_modules/@exabyte-io/periodic-table.js": {
             "version": "2023.12.23-3",
             "resolved": "https://registry.npmjs.org/@exabyte-io/periodic-table.js/-/periodic-table.js-2023.12.23-3.tgz",
@@ -3660,47 +3553,6 @@
             "dev": true,
             "license": "MIT"
         },
-        "node_modules/@humanwhocodes/config-array": {
-            "version": "0.13.0",
-            "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
-            "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==",
-            "deprecated": "Use @eslint/config-array instead",
-            "dev": true,
-            "license": "Apache-2.0",
-            "peer": true,
-            "dependencies": {
-                "@humanwhocodes/object-schema": "^2.0.3",
-                "debug": "^4.3.1",
-                "minimatch": "^3.0.5"
-            },
-            "engines": {
-                "node": ">=10.10.0"
-            }
-        },
-        "node_modules/@humanwhocodes/module-importer": {
-            "version": "1.0.1",
-            "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
-            "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
-            "dev": true,
-            "license": "Apache-2.0",
-            "peer": true,
-            "engines": {
-                "node": ">=12.22"
-            },
-            "funding": {
-                "type": "github",
-                "url": "https://github.com/sponsors/nzakas"
-            }
-        },
-        "node_modules/@humanwhocodes/object-schema": {
-            "version": "2.0.3",
-            "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz",
-            "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==",
-            "deprecated": "Use @eslint/object-schema instead",
-            "dev": true,
-            "license": "BSD-3-Clause",
-            "peer": true
-        },
         "node_modules/@jridgewell/gen-mapping": {
             "version": "0.3.13",
             "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
@@ -4486,9 +4338,9 @@
             }
         },
         "node_modules/@mat3ra/ide": {
-            "version": "0.0.0",
-            "resolved": "https://github.com/mat3ra/ide/releases/download/wip-88b8881/ide.tgz",
-            "integrity": "sha512-28neYHQrSoAqbjsrDmg9PsUi6D1L6PwYgI/tjehrzHmZwYxt6bQVpnIgO/S9lvKBbriwaUCkHJ1vFoXtAL1Umg==",
+            "version": "2026.8.18-0",
+            "resolved": "https://registry.npmjs.org/@mat3ra/ide/-/ide-2026.8.18-0.tgz",
+            "integrity": "sha512-xbV7KlxtFer+ailbZJwTbZttcjSLtIgw4BuQLFt+2VCAki67ojazbHqNAPCV1E0xxMQ+7X6NACOK8vA8ySjukw==",
             "dev": true,
             "license": "Apache-2.0",
             "dependencies": {
@@ -4842,9 +4694,9 @@
             }
         },
         "node_modules/@mat3ra/made": {
-            "version": "0.0.0",
-            "resolved": "https://github.com/mat3ra/made/releases/download/wip-b5ab8c9/made.tgz",
-            "integrity": "sha512-UdhFBnX/4CZOIeMoCKh0H5O1JfTCAaxDrM11y4Mah8Cdql3fZErYJ9k8Jsfv7T4czBmlnZW9rLjhpm3jUTIgug==",
+            "version": "2026.8.18-0",
+            "resolved": "https://registry.npmjs.org/@mat3ra/made/-/made-2026.8.18-0.tgz",
+            "integrity": "sha512-00rb8HU7Kt8B9wARuJbhbfxFvK2KG0+aF8wJImMqV3jusUZ0qGp0sfQf3EYc68Z9VFMnSYMgiKROm9RajlfmfQ==",
             "dev": true,
             "license": "Apache-2.0",
             "dependencies": {
@@ -4882,9 +4734,9 @@
             }
         },
         "node_modules/@mat3ra/mode": {
-            "version": "0.0.0",
-            "resolved": "https://github.com/mat3ra/mode/releases/download/wip-f8543e7/mode.tgz",
-            "integrity": "sha512-uz8/4TRfa3eHBwZyi6Dre3BocTpr0bkLiUliKjFE3/k9YI3GKe/O675VSTGgQVFekcCxJMJuz/CsE+99rdD+LQ==",
+            "version": "2026.8.18-0",
+            "resolved": "https://registry.npmjs.org/@mat3ra/mode/-/mode-2026.8.18-0.tgz",
+            "integrity": "sha512-Q2nhBaS8tibFTivaOzQRh5Z3FbqkIIfYhTMKlnnM/sn+UJZPSal8KvUBaHEOUo8Enxe3NxB3oluX+u17vUo0yA==",
             "dev": true,
             "license": "Apache-2.0",
             "dependencies": {
@@ -4896,7 +4748,7 @@
                 "@babel/preset-react": "7.16.7",
                 "@babel/register": "^7.16.0",
                 "@babel/runtime-corejs3": "7.16.8",
-                "@mat3ra/utils": "https://github.com/mat3ra/utils/releases/download/wip-8b2c388/utils.tgz",
+                "@mat3ra/utils": "2026.8.18-1",
                 "@types/lodash": "^4.14.202",
                 "@types/node": "^20.11.30",
                 "@types/underscore": "^1.11.14",
@@ -5195,10 +5047,116 @@
                 "node": ">=6.9.0"
             }
         },
-        "node_modules/@mat3ra/mode/node_modules/@mat3ra/utils": {
-            "version": "0.0.0",
-            "resolved": "https://github.com/mat3ra/utils/releases/download/wip-8b2c388/utils.tgz",
-            "integrity": "sha512-vzShVrwuC16hi2HQiZsf7sGwnbu6+azHHsshd3OUVKnup+5vlZuIiFdUTXyK1jU5hoxiecCo2Wqb22UV8vlrzQ==",
+        "node_modules/@mat3ra/mode/node_modules/babel-plugin-polyfill-corejs2": {
+            "version": "0.3.3",
+            "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.3.tgz",
+            "integrity": "sha512-8hOdmFYFSZhqg2C/JgLUQ+t52o5nirNwaWM2B9LWteozwIvM14VSwdsCAUET10qT+kmySAlseadmfeeSWFCy+Q==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "@babel/compat-data": "^7.17.7",
+                "@babel/helper-define-polyfill-provider": "^0.3.3",
+                "semver": "^6.1.1"
+            },
+            "peerDependencies": {
+                "@babel/core": "^7.0.0-0"
+            }
+        },
+        "node_modules/@mat3ra/mode/node_modules/babel-plugin-polyfill-corejs3": {
+            "version": "0.4.0",
+            "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.4.0.tgz",
+            "integrity": "sha512-YxFreYwUfglYKdLUGvIF2nJEsGwj+RhWSX/ije3D2vQPOXuyMLMtg/cCGMDpOA7Nd+MwlNdnGODbd2EwUZPlsw==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "@babel/helper-define-polyfill-provider": "^0.3.0",
+                "core-js-compat": "^3.18.0"
+            },
+            "peerDependencies": {
+                "@babel/core": "^7.0.0-0"
+            }
+        },
+        "node_modules/@mat3ra/mode/node_modules/babel-plugin-polyfill-regenerator": {
+            "version": "0.3.1",
+            "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.3.1.tgz",
+            "integrity": "sha512-Y2B06tvgHYt1x0yz17jGkGeeMr5FeKUu+ASJ+N6nB5lQ8Dapfg42i0OVrf8PNGJ3zKL4A23snMi1IRwrqqND7A==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "@babel/helper-define-polyfill-provider": "^0.3.1"
+            },
+            "peerDependencies": {
+                "@babel/core": "^7.0.0-0"
+            }
+        },
+        "node_modules/@mat3ra/mode/node_modules/commander": {
+            "version": "4.1.1",
+            "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
+            "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
+            "dev": true,
+            "license": "MIT",
+            "engines": {
+                "node": ">= 6"
+            }
+        },
+        "node_modules/@mat3ra/prode": {
+            "version": "2026.8.18-0",
+            "resolved": "https://registry.npmjs.org/@mat3ra/prode/-/prode-2026.8.18-0.tgz",
+            "integrity": "sha512-BwsNYt++e/gIXrsawSlnSodoV304lZ8EybZGKeX9JQq/tWQEvz6SQDHpVMRsxEqS6aujIknKyh5YiVxAmo47Nw==",
+            "dev": true,
+            "license": "Apache-2.0",
+            "dependencies": {
+                "@types/highcharts": "^5.0.44",
+                "lodash": "^4.17.21"
+            },
+            "engines": {
+                "node": ">=14.0.0"
+            },
+            "peerDependencies": {
+                "@mat3ra/code": "*",
+                "@mat3ra/esse": "*",
+                "@mat3ra/made": "*"
+            }
+        },
+        "node_modules/@mat3ra/standata": {
+            "version": "2026.8.18-0",
+            "resolved": "https://registry.npmjs.org/@mat3ra/standata/-/standata-2026.8.18-0.tgz",
+            "integrity": "sha512-EltpDJnQnz9KcyvqdYHpwNCcpCciRAXWs6uKiCum1nstti1vHdbmTkkw7h0MWbdU2qa5SDX2I0aM0NSsPzfpTA==",
+            "dev": true,
+            "license": "Apache-2.0",
+            "dependencies": {
+                "@types/nunjucks": "^3.2.6",
+                "@types/sprintf-js": "^1.1.4",
+                "cmd-ts": "^0.15.0",
+                "compare-versions": "^6.1.1",
+                "js-yaml": "^4.3.0",
+                "lodash": "^4.18.1",
+                "nunjucks": "^3.2.4",
+                "sprintf-js": "^1.1.3",
+                "ts-node": "^10.9.2",
+                "typescript": "^5.9.3"
+            },
+            "engines": {
+                "node": ">=20.0.0"
+            },
+            "peerDependencies": {
+                "@mat3ra/esse": "*"
+            }
+        },
+        "node_modules/@mat3ra/tsconfig": {
+            "version": "2024.6.3-0",
+            "resolved": "https://registry.npmjs.org/@mat3ra/tsconfig/-/tsconfig-2024.6.3-0.tgz",
+            "integrity": "sha512-dA3gKDSl9+vlZvot1DN+UXfZPgxPSyHkZrkfVCTOWvzwm/3NdJ4cTzrTxjBIWUrZukpVVtH5ao8FbXLB6RDQzQ==",
+            "dev": true,
+            "license": "Apache-2.0",
+            "engines": {
+                "node": ">=14.0.0"
+            }
+        },
+        "node_modules/@mat3ra/utils": {
+            "version": "2026.8.18-1",
+            "resolved": "https://registry.npmjs.org/@mat3ra/utils/-/utils-2026.8.18-1.tgz",
+            "integrity": "sha512-0J9s42sL/2S9tuwjUbxwo6/ORlGCbCvEqYSmHZgSmHNfzIc2mwuiXRNF0S5Al9EJDfYy6VO4WabDCrfpvCci8A==",
             "dev": true,
             "license": "ISC",
             "dependencies": {
@@ -5226,975 +5184,41 @@
                 "node": ">=20.0.0"
             }
         },
-        "node_modules/@mat3ra/mode/node_modules/@mat3ra/utils/node_modules/@babel/cli": {
-            "version": "7.29.7",
-            "resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.29.7.tgz",
-            "integrity": "sha512-/75HwRbAYPqXv/Ax1h7Fg3IZfXgdU98jnA8H93/m/QBaPV3Hp5ICoLqzGYye1yHBCgpmXvtqgSUN8oOKX5tojQ==",
+        "node_modules/@mat3ra/utils/node_modules/mathjs": {
+            "version": "12.4.3",
+            "resolved": "https://registry.npmjs.org/mathjs/-/mathjs-12.4.3.tgz",
+            "integrity": "sha512-oHdGPDbp7gO873xxG90RLq36IuicuKvbpr/bBG5g9c8Obm/VsKVrK9uoRZZHUodohzlnmCEqfDzbR3LH6m+aAQ==",
             "dev": true,
-            "license": "MIT",
+            "license": "Apache-2.0",
             "dependencies": {
-                "@jridgewell/trace-mapping": "^0.3.28",
-                "commander": "^6.2.0",
-                "convert-source-map": "^2.0.0",
-                "fs-readdir-recursive": "^1.1.0",
-                "glob": "^7.2.0",
-                "make-dir": "^2.1.0",
-                "slash": "^2.0.0"
+                "@babel/runtime": "^7.24.4",
+                "complex.js": "^2.1.1",
+                "decimal.js": "^10.4.3",
+                "escape-latex": "^1.2.0",
+                "fraction.js": "4.3.4",
+                "javascript-natural-sort": "^0.7.1",
+                "seedrandom": "^3.0.5",
+                "tiny-emitter": "^2.1.0",
+                "typed-function": "^4.1.1"
             },
             "bin": {
-                "babel": "bin/babel.js",
-                "babel-external-helpers": "bin/babel-external-helpers.js"
+                "mathjs": "bin/cli.js"
             },
             "engines": {
-                "node": ">=6.9.0"
-            },
-            "optionalDependencies": {
-                "@nicolo-ribaudo/chokidar-2": "2.1.8-no-fsevents.3",
-                "chokidar": "^3.6.0"
-            },
-            "peerDependencies": {
-                "@babel/core": "^7.0.0-0"
+                "node": ">= 18"
             }
         },
-        "node_modules/@mat3ra/mode/node_modules/@mat3ra/utils/node_modules/@babel/core": {
-            "version": "7.29.7",
-            "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz",
-            "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
+        "node_modules/@mat3ra/utils/node_modules/semver": {
+            "version": "7.8.5",
+            "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
+            "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
             "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "@babel/code-frame": "^7.29.7",
-                "@babel/generator": "^7.29.7",
-                "@babel/helper-compilation-targets": "^7.29.7",
-                "@babel/helper-module-transforms": "^7.29.7",
-                "@babel/helpers": "^7.29.7",
-                "@babel/parser": "^7.29.7",
-                "@babel/template": "^7.29.7",
-                "@babel/traverse": "^7.29.7",
-                "@babel/types": "^7.29.7",
-                "@jridgewell/remapping": "^2.3.5",
-                "convert-source-map": "^2.0.0",
-                "debug": "^4.1.0",
-                "gensync": "^1.0.0-beta.2",
-                "json5": "^2.2.3",
-                "semver": "^6.3.1"
+            "license": "ISC",
+            "bin": {
+                "semver": "bin/semver.js"
             },
             "engines": {
-                "node": ">=6.9.0"
-            },
-            "funding": {
-                "type": "opencollective",
-                "url": "https://opencollective.com/babel"
-            }
-        },
-        "node_modules/@mat3ra/mode/node_modules/@mat3ra/utils/node_modules/@babel/core/node_modules/semver": {
-            "version": "6.3.1",
-            "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
-            "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
-            "dev": true,
-            "license": "ISC",
-            "bin": {
-                "semver": "bin/semver.js"
-            }
-        },
-        "node_modules/@mat3ra/mode/node_modules/@mat3ra/utils/node_modules/@babel/eslint-parser": {
-            "version": "7.29.7",
-            "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.29.7.tgz",
-            "integrity": "sha512-zxt+UJTOMKvUt3yOg+D58MLuz334pHp93qifMFcjIIO+9hN6t+ufw2gi7vDPMpxvfnHRR+3VVXvIjineCcgyXw==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1",
-                "eslint-visitor-keys": "^2.1.0",
-                "semver": "^6.3.1"
-            },
-            "engines": {
-                "node": "^10.13.0 || ^12.13.0 || >=14.0.0"
-            },
-            "peerDependencies": {
-                "@babel/core": "^7.11.0",
-                "eslint": "^7.5.0 || ^8.0.0 || ^9.0.0"
-            }
-        },
-        "node_modules/@mat3ra/mode/node_modules/@mat3ra/utils/node_modules/@babel/eslint-parser/node_modules/semver": {
-            "version": "6.3.1",
-            "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
-            "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
-            "dev": true,
-            "license": "ISC",
-            "bin": {
-                "semver": "bin/semver.js"
-            }
-        },
-        "node_modules/@mat3ra/mode/node_modules/@mat3ra/utils/node_modules/@babel/helper-define-polyfill-provider": {
-            "version": "0.6.8",
-            "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.8.tgz",
-            "integrity": "sha512-47UwBLPpQi1NoWzLuHNjRoHlYXMwIJoBf7MFou6viC/sIHWYygpvr0B6IAyh5sBdA2nr2LPIRww8lfaUVQINBA==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "@babel/helper-compilation-targets": "^7.28.6",
-                "@babel/helper-plugin-utils": "^7.28.6",
-                "debug": "^4.4.3",
-                "lodash.debounce": "^4.0.8",
-                "resolve": "^1.22.11"
-            },
-            "peerDependencies": {
-                "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
-            }
-        },
-        "node_modules/@mat3ra/mode/node_modules/@mat3ra/utils/node_modules/@babel/plugin-proposal-class-properties": {
-            "version": "7.18.6",
-            "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz",
-            "integrity": "sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==",
-            "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-properties instead.",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "@babel/helper-create-class-features-plugin": "^7.18.6",
-                "@babel/helper-plugin-utils": "^7.18.6"
-            },
-            "engines": {
-                "node": ">=6.9.0"
-            },
-            "peerDependencies": {
-                "@babel/core": "^7.0.0-0"
-            }
-        },
-        "node_modules/@mat3ra/mode/node_modules/@mat3ra/utils/node_modules/@babel/plugin-proposal-private-property-in-object": {
-            "version": "7.21.0-placeholder-for-preset-env.2",
-            "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz",
-            "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==",
-            "dev": true,
-            "license": "MIT",
-            "engines": {
-                "node": ">=6.9.0"
-            },
-            "peerDependencies": {
-                "@babel/core": "^7.0.0-0"
-            }
-        },
-        "node_modules/@mat3ra/mode/node_modules/@mat3ra/utils/node_modules/@babel/preset-env": {
-            "version": "7.29.7",
-            "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.29.7.tgz",
-            "integrity": "sha512-GYzX36n1nsciIb0uyH0GHwxwtNwPQIcpxSeiVLDtG/B7jB5xXgchnmL1f/jCX5o+pwnaDBtO60ONSJhEBJfxYA==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "@babel/compat-data": "^7.29.7",
-                "@babel/helper-compilation-targets": "^7.29.7",
-                "@babel/helper-plugin-utils": "^7.29.7",
-                "@babel/helper-validator-option": "^7.29.7",
-                "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.29.7",
-                "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.29.7",
-                "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.29.7",
-                "@babel/plugin-bugfix-safari-rest-destructuring-rhs-array": "^7.29.7",
-                "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.29.7",
-                "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.29.7",
-                "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2",
-                "@babel/plugin-syntax-import-assertions": "^7.29.7",
-                "@babel/plugin-syntax-import-attributes": "^7.29.7",
-                "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6",
-                "@babel/plugin-transform-arrow-functions": "^7.29.7",
-                "@babel/plugin-transform-async-generator-functions": "^7.29.7",
-                "@babel/plugin-transform-async-to-generator": "^7.29.7",
-                "@babel/plugin-transform-block-scoped-functions": "^7.29.7",
-                "@babel/plugin-transform-block-scoping": "^7.29.7",
-                "@babel/plugin-transform-class-properties": "^7.29.7",
-                "@babel/plugin-transform-class-static-block": "^7.29.7",
-                "@babel/plugin-transform-classes": "^7.29.7",
-                "@babel/plugin-transform-computed-properties": "^7.29.7",
-                "@babel/plugin-transform-destructuring": "^7.29.7",
-                "@babel/plugin-transform-dotall-regex": "^7.29.7",
-                "@babel/plugin-transform-duplicate-keys": "^7.29.7",
-                "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.29.7",
-                "@babel/plugin-transform-dynamic-import": "^7.29.7",
-                "@babel/plugin-transform-explicit-resource-management": "^7.29.7",
-                "@babel/plugin-transform-exponentiation-operator": "^7.29.7",
-                "@babel/plugin-transform-export-namespace-from": "^7.29.7",
-                "@babel/plugin-transform-for-of": "^7.29.7",
-                "@babel/plugin-transform-function-name": "^7.29.7",
-                "@babel/plugin-transform-json-strings": "^7.29.7",
-                "@babel/plugin-transform-literals": "^7.29.7",
-                "@babel/plugin-transform-logical-assignment-operators": "^7.29.7",
-                "@babel/plugin-transform-member-expression-literals": "^7.29.7",
-                "@babel/plugin-transform-modules-amd": "^7.29.7",
-                "@babel/plugin-transform-modules-commonjs": "^7.29.7",
-                "@babel/plugin-transform-modules-systemjs": "^7.29.7",
-                "@babel/plugin-transform-modules-umd": "^7.29.7",
-                "@babel/plugin-transform-named-capturing-groups-regex": "^7.29.7",
-                "@babel/plugin-transform-new-target": "^7.29.7",
-                "@babel/plugin-transform-nullish-coalescing-operator": "^7.29.7",
-                "@babel/plugin-transform-numeric-separator": "^7.29.7",
-                "@babel/plugin-transform-object-rest-spread": "^7.29.7",
-                "@babel/plugin-transform-object-super": "^7.29.7",
-                "@babel/plugin-transform-optional-catch-binding": "^7.29.7",
-                "@babel/plugin-transform-optional-chaining": "^7.29.7",
-                "@babel/plugin-transform-parameters": "^7.29.7",
-                "@babel/plugin-transform-private-methods": "^7.29.7",
-                "@babel/plugin-transform-private-property-in-object": "^7.29.7",
-                "@babel/plugin-transform-property-literals": "^7.29.7",
-                "@babel/plugin-transform-regenerator": "^7.29.7",
-                "@babel/plugin-transform-regexp-modifiers": "^7.29.7",
-                "@babel/plugin-transform-reserved-words": "^7.29.7",
-                "@babel/plugin-transform-shorthand-properties": "^7.29.7",
-                "@babel/plugin-transform-spread": "^7.29.7",
-                "@babel/plugin-transform-sticky-regex": "^7.29.7",
-                "@babel/plugin-transform-template-literals": "^7.29.7",
-                "@babel/plugin-transform-typeof-symbol": "^7.29.7",
-                "@babel/plugin-transform-unicode-escapes": "^7.29.7",
-                "@babel/plugin-transform-unicode-property-regex": "^7.29.7",
-                "@babel/plugin-transform-unicode-regex": "^7.29.7",
-                "@babel/plugin-transform-unicode-sets-regex": "^7.29.7",
-                "@babel/preset-modules": "0.1.6-no-external-plugins",
-                "babel-plugin-polyfill-corejs2": "^0.4.15",
-                "babel-plugin-polyfill-corejs3": "^0.14.0",
-                "babel-plugin-polyfill-regenerator": "^0.6.6",
-                "core-js-compat": "^3.48.0",
-                "semver": "^6.3.1"
-            },
-            "engines": {
-                "node": ">=6.9.0"
-            },
-            "peerDependencies": {
-                "@babel/core": "^7.0.0-0"
-            }
-        },
-        "node_modules/@mat3ra/mode/node_modules/@mat3ra/utils/node_modules/@babel/preset-env/node_modules/semver": {
-            "version": "6.3.1",
-            "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
-            "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
-            "dev": true,
-            "license": "ISC",
-            "bin": {
-                "semver": "bin/semver.js"
-            }
-        },
-        "node_modules/@mat3ra/mode/node_modules/@mat3ra/utils/node_modules/@babel/preset-modules": {
-            "version": "0.1.6-no-external-plugins",
-            "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz",
-            "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "@babel/helper-plugin-utils": "^7.0.0",
-                "@babel/types": "^7.4.4",
-                "esutils": "^2.0.2"
-            },
-            "peerDependencies": {
-                "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0"
-            }
-        },
-        "node_modules/@mat3ra/mode/node_modules/@mat3ra/utils/node_modules/@babel/preset-react": {
-            "version": "7.29.7",
-            "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.29.7.tgz",
-            "integrity": "sha512-C+PV1TFUPTmBQGoPBL8j2QmLpZ117YTCwxIZeJOM96GbYMFSc7/pOXU5lVykwnZxyTqQxRsvoRk6f2FktZgGHA==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "@babel/helper-plugin-utils": "^7.29.7",
-                "@babel/helper-validator-option": "^7.29.7",
-                "@babel/plugin-transform-react-display-name": "^7.29.7",
-                "@babel/plugin-transform-react-jsx": "^7.29.7",
-                "@babel/plugin-transform-react-jsx-development": "^7.29.7",
-                "@babel/plugin-transform-react-pure-annotations": "^7.29.7"
-            },
-            "engines": {
-                "node": ">=6.9.0"
-            },
-            "peerDependencies": {
-                "@babel/core": "^7.0.0-0"
-            }
-        },
-        "node_modules/@mat3ra/mode/node_modules/@mat3ra/utils/node_modules/@babel/runtime-corejs3": {
-            "version": "7.29.7",
-            "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.29.7.tgz",
-            "integrity": "sha512-ppj9ouYku+RX0ljtgZd+KMO5mkM2bCqg8H2PYAFWnLsHEIKIdRojqbJ2i3eVHrisuxy7nOFCmngTDdWtUCdXUQ==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "core-js-pure": "^3.48.0"
-            },
-            "engines": {
-                "node": ">=6.9.0"
-            }
-        },
-        "node_modules/@mat3ra/mode/node_modules/@mat3ra/utils/node_modules/babel-plugin-polyfill-corejs2": {
-            "version": "0.4.17",
-            "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.17.tgz",
-            "integrity": "sha512-aTyf30K/rqAsNwN76zYrdtx8obu0E4KoUME29B1xj+B3WxgvWkp943vYQ+z8Mv3lw9xHXMHpvSPOBxzAkIa94w==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "@babel/compat-data": "^7.28.6",
-                "@babel/helper-define-polyfill-provider": "^0.6.8",
-                "semver": "^6.3.1"
-            },
-            "peerDependencies": {
-                "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
-            }
-        },
-        "node_modules/@mat3ra/mode/node_modules/@mat3ra/utils/node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": {
-            "version": "6.3.1",
-            "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
-            "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
-            "dev": true,
-            "license": "ISC",
-            "bin": {
-                "semver": "bin/semver.js"
-            }
-        },
-        "node_modules/@mat3ra/mode/node_modules/@mat3ra/utils/node_modules/babel-plugin-polyfill-corejs3": {
-            "version": "0.14.2",
-            "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.14.2.tgz",
-            "integrity": "sha512-coWpDLJ410R781Npmn/SIBZEsAetR4xVi0SxLMXPaMO4lSf1MwnkGYMtkFxew0Dn8B3/CpbpYxN0JCgg8mn67g==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "@babel/helper-define-polyfill-provider": "^0.6.8",
-                "core-js-compat": "^3.48.0"
-            },
-            "peerDependencies": {
-                "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
-            }
-        },
-        "node_modules/@mat3ra/mode/node_modules/@mat3ra/utils/node_modules/babel-plugin-polyfill-regenerator": {
-            "version": "0.6.8",
-            "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.8.tgz",
-            "integrity": "sha512-M762rNHfSF1EV3SLtnCJXFoQbbIIz0OyRwnCmV0KPC7qosSfCO0QLTSuJX3ayAebubhE6oYBAYPrBA5ljowaZg==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "@babel/helper-define-polyfill-provider": "^0.6.8"
-            },
-            "peerDependencies": {
-                "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
-            }
-        },
-        "node_modules/@mat3ra/mode/node_modules/@mat3ra/utils/node_modules/commander": {
-            "version": "6.2.1",
-            "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz",
-            "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==",
-            "dev": true,
-            "license": "MIT",
-            "engines": {
-                "node": ">= 6"
-            }
-        },
-        "node_modules/@mat3ra/mode/node_modules/@mat3ra/utils/node_modules/convert-source-map": {
-            "version": "2.0.0",
-            "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
-            "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
-            "dev": true,
-            "license": "MIT"
-        },
-        "node_modules/@mat3ra/mode/node_modules/@mat3ra/utils/node_modules/semver": {
-            "version": "7.8.5",
-            "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
-            "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
-            "dev": true,
-            "license": "ISC",
-            "bin": {
-                "semver": "bin/semver.js"
-            },
-            "engines": {
-                "node": ">=10"
-            }
-        },
-        "node_modules/@mat3ra/mode/node_modules/@mat3ra/utils/node_modules/typescript": {
-            "version": "4.9.5",
-            "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
-            "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
-            "dev": true,
-            "license": "Apache-2.0",
-            "bin": {
-                "tsc": "bin/tsc",
-                "tsserver": "bin/tsserver"
-            },
-            "engines": {
-                "node": ">=4.2.0"
-            }
-        },
-        "node_modules/@mat3ra/mode/node_modules/babel-plugin-polyfill-corejs2": {
-            "version": "0.3.3",
-            "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.3.tgz",
-            "integrity": "sha512-8hOdmFYFSZhqg2C/JgLUQ+t52o5nirNwaWM2B9LWteozwIvM14VSwdsCAUET10qT+kmySAlseadmfeeSWFCy+Q==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "@babel/compat-data": "^7.17.7",
-                "@babel/helper-define-polyfill-provider": "^0.3.3",
-                "semver": "^6.1.1"
-            },
-            "peerDependencies": {
-                "@babel/core": "^7.0.0-0"
-            }
-        },
-        "node_modules/@mat3ra/mode/node_modules/babel-plugin-polyfill-corejs3": {
-            "version": "0.4.0",
-            "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.4.0.tgz",
-            "integrity": "sha512-YxFreYwUfglYKdLUGvIF2nJEsGwj+RhWSX/ije3D2vQPOXuyMLMtg/cCGMDpOA7Nd+MwlNdnGODbd2EwUZPlsw==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "@babel/helper-define-polyfill-provider": "^0.3.0",
-                "core-js-compat": "^3.18.0"
-            },
-            "peerDependencies": {
-                "@babel/core": "^7.0.0-0"
-            }
-        },
-        "node_modules/@mat3ra/mode/node_modules/babel-plugin-polyfill-regenerator": {
-            "version": "0.3.1",
-            "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.3.1.tgz",
-            "integrity": "sha512-Y2B06tvgHYt1x0yz17jGkGeeMr5FeKUu+ASJ+N6nB5lQ8Dapfg42i0OVrf8PNGJ3zKL4A23snMi1IRwrqqND7A==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "@babel/helper-define-polyfill-provider": "^0.3.1"
-            },
-            "peerDependencies": {
-                "@babel/core": "^7.0.0-0"
-            }
-        },
-        "node_modules/@mat3ra/mode/node_modules/commander": {
-            "version": "4.1.1",
-            "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
-            "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
-            "dev": true,
-            "license": "MIT",
-            "engines": {
-                "node": ">= 6"
-            }
-        },
-        "node_modules/@mat3ra/mode/node_modules/mathjs": {
-            "version": "12.4.3",
-            "resolved": "https://registry.npmjs.org/mathjs/-/mathjs-12.4.3.tgz",
-            "integrity": "sha512-oHdGPDbp7gO873xxG90RLq36IuicuKvbpr/bBG5g9c8Obm/VsKVrK9uoRZZHUodohzlnmCEqfDzbR3LH6m+aAQ==",
-            "dev": true,
-            "license": "Apache-2.0",
-            "dependencies": {
-                "@babel/runtime": "^7.24.4",
-                "complex.js": "^2.1.1",
-                "decimal.js": "^10.4.3",
-                "escape-latex": "^1.2.0",
-                "fraction.js": "4.3.4",
-                "javascript-natural-sort": "^0.7.1",
-                "seedrandom": "^3.0.5",
-                "tiny-emitter": "^2.1.0",
-                "typed-function": "^4.1.1"
-            },
-            "bin": {
-                "mathjs": "bin/cli.js"
-            },
-            "engines": {
-                "node": ">= 18"
-            }
-        },
-        "node_modules/@mat3ra/prode": {
-            "version": "2026.8.18-0",
-            "resolved": "https://registry.npmjs.org/@mat3ra/prode/-/prode-2026.8.18-0.tgz",
-            "integrity": "sha512-BwsNYt++e/gIXrsawSlnSodoV304lZ8EybZGKeX9JQq/tWQEvz6SQDHpVMRsxEqS6aujIknKyh5YiVxAmo47Nw==",
-            "dev": true,
-            "license": "Apache-2.0",
-            "dependencies": {
-                "@types/highcharts": "^5.0.44",
-                "lodash": "^4.17.21"
-            },
-            "engines": {
-                "node": ">=14.0.0"
-            },
-            "peerDependencies": {
-                "@mat3ra/code": "*",
-                "@mat3ra/esse": "*",
-                "@mat3ra/made": "*"
-            }
-        },
-        "node_modules/@mat3ra/standata": {
-            "version": "2026.8.18-0",
-            "resolved": "https://registry.npmjs.org/@mat3ra/standata/-/standata-2026.8.18-0.tgz",
-            "integrity": "sha512-EltpDJnQnz9KcyvqdYHpwNCcpCciRAXWs6uKiCum1nstti1vHdbmTkkw7h0MWbdU2qa5SDX2I0aM0NSsPzfpTA==",
-            "dev": true,
-            "license": "Apache-2.0",
-            "dependencies": {
-                "@types/nunjucks": "^3.2.6",
-                "@types/sprintf-js": "^1.1.4",
-                "cmd-ts": "^0.15.0",
-                "compare-versions": "^6.1.1",
-                "js-yaml": "^4.3.0",
-                "lodash": "^4.18.1",
-                "nunjucks": "^3.2.4",
-                "sprintf-js": "^1.1.3",
-                "ts-node": "^10.9.2",
-                "typescript": "^5.9.3"
-            },
-            "engines": {
-                "node": ">=20.0.0"
-            },
-            "peerDependencies": {
-                "@mat3ra/esse": "*"
-            }
-        },
-        "node_modules/@mat3ra/tsconfig": {
-            "version": "2024.6.3-0",
-            "resolved": "https://registry.npmjs.org/@mat3ra/tsconfig/-/tsconfig-2024.6.3-0.tgz",
-            "integrity": "sha512-dA3gKDSl9+vlZvot1DN+UXfZPgxPSyHkZrkfVCTOWvzwm/3NdJ4cTzrTxjBIWUrZukpVVtH5ao8FbXLB6RDQzQ==",
-            "dev": true,
-            "license": "Apache-2.0",
-            "engines": {
-                "node": ">=14.0.0"
-            }
-        },
-        "node_modules/@mat3ra/utils": {
-            "version": "2026.4.15-1",
-            "resolved": "https://registry.npmjs.org/@mat3ra/utils/-/utils-2026.4.15-1.tgz",
-            "integrity": "sha512-ahIUAc9QzbZ+G3rvIPW5BqAwa4pPMjDwH4k3+1SrXSYUhM67z+S4FTpcsfNn4WkAgmJ2Az9l026RvP3xZcXF9g==",
-            "dev": true,
-            "license": "ISC",
-            "peer": true,
-            "dependencies": {
-                "@babel/cli": "7.16.0",
-                "@babel/core": "7.24.1",
-                "@babel/eslint-parser": "7.16.3",
-                "@babel/plugin-proposal-class-properties": "7.16.0",
-                "@babel/preset-env": "7.16.4",
-                "@babel/preset-react": "7.16.7",
-                "@babel/preset-typescript": "^7.22.5",
-                "@babel/register": "^7.16.0",
-                "@babel/runtime-corejs3": "7.16.8",
-                "crypto-js": "^4.1.1",
-                "js-yaml": "^4.1.0",
-                "lodash": "^4.17.21",
-                "mathjs": "12.4.1",
-                "semver": "^7.5.3",
-                "ts-node": "^10.9.1",
-                "typescript": "^4.5.5",
-                "underscore": "^1.13.3",
-                "underscore.string": "^3.3.4",
-                "uuid": "8.3.2"
-            },
-            "engines": {
-                "node": ">=20.0.0"
-            }
-        },
-        "node_modules/@mat3ra/utils/node_modules/@babel/cli": {
-            "version": "7.16.0",
-            "resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.16.0.tgz",
-            "integrity": "sha512-WLrM42vKX/4atIoQB+eb0ovUof53UUvecb4qGjU2PDDWRiZr50ZpiV8NpcLo7iSxeGYrRG0Mqembsa+UrTAV6Q==",
-            "dev": true,
-            "license": "MIT",
-            "peer": true,
-            "dependencies": {
-                "commander": "^4.0.1",
-                "convert-source-map": "^1.1.0",
-                "fs-readdir-recursive": "^1.1.0",
-                "glob": "^7.0.0",
-                "make-dir": "^2.1.0",
-                "slash": "^2.0.0",
-                "source-map": "^0.5.0"
-            },
-            "bin": {
-                "babel": "bin/babel.js",
-                "babel-external-helpers": "bin/babel-external-helpers.js"
-            },
-            "engines": {
-                "node": ">=6.9.0"
-            },
-            "optionalDependencies": {
-                "@nicolo-ribaudo/chokidar-2": "2.1.8-no-fsevents.3",
-                "chokidar": "^3.4.0"
-            },
-            "peerDependencies": {
-                "@babel/core": "^7.0.0-0"
-            }
-        },
-        "node_modules/@mat3ra/utils/node_modules/@babel/core": {
-            "version": "7.24.1",
-            "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.1.tgz",
-            "integrity": "sha512-F82udohVyIgGAY2VVj/g34TpFUG606rumIHjTfVbssPg2zTR7PuuEpZcX8JA6sgBfIYmJrFtWgPvHQuJamVqZQ==",
-            "dev": true,
-            "license": "MIT",
-            "peer": true,
-            "dependencies": {
-                "@ampproject/remapping": "^2.2.0",
-                "@babel/code-frame": "^7.24.1",
-                "@babel/generator": "^7.24.1",
-                "@babel/helper-compilation-targets": "^7.23.6",
-                "@babel/helper-module-transforms": "^7.23.3",
-                "@babel/helpers": "^7.24.1",
-                "@babel/parser": "^7.24.1",
-                "@babel/template": "^7.24.0",
-                "@babel/traverse": "^7.24.1",
-                "@babel/types": "^7.24.0",
-                "convert-source-map": "^2.0.0",
-                "debug": "^4.1.0",
-                "gensync": "^1.0.0-beta.2",
-                "json5": "^2.2.3",
-                "semver": "^6.3.1"
-            },
-            "engines": {
-                "node": ">=6.9.0"
-            },
-            "funding": {
-                "type": "opencollective",
-                "url": "https://opencollective.com/babel"
-            }
-        },
-        "node_modules/@mat3ra/utils/node_modules/@babel/core/node_modules/convert-source-map": {
-            "version": "2.0.0",
-            "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
-            "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
-            "dev": true,
-            "license": "MIT",
-            "peer": true
-        },
-        "node_modules/@mat3ra/utils/node_modules/@babel/core/node_modules/semver": {
-            "version": "6.3.1",
-            "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
-            "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
-            "dev": true,
-            "license": "ISC",
-            "peer": true,
-            "bin": {
-                "semver": "bin/semver.js"
-            }
-        },
-        "node_modules/@mat3ra/utils/node_modules/@babel/eslint-parser": {
-            "version": "7.16.3",
-            "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.16.3.tgz",
-            "integrity": "sha512-iB4ElZT0jAt7PKVaeVulOECdGe6UnmA/O0P9jlF5g5GBOwDVbna8AXhHRu4s27xQf6OkveyA8iTDv1jHdDejgQ==",
-            "dev": true,
-            "license": "MIT",
-            "peer": true,
-            "dependencies": {
-                "eslint-scope": "^5.1.1",
-                "eslint-visitor-keys": "^2.1.0",
-                "semver": "^6.3.0"
-            },
-            "engines": {
-                "node": "^10.13.0 || ^12.13.0 || >=14.0.0"
-            },
-            "peerDependencies": {
-                "@babel/core": ">=7.11.0",
-                "eslint": "^7.5.0 || ^8.0.0"
-            }
-        },
-        "node_modules/@mat3ra/utils/node_modules/@babel/eslint-parser/node_modules/semver": {
-            "version": "6.3.1",
-            "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
-            "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
-            "dev": true,
-            "license": "ISC",
-            "peer": true,
-            "bin": {
-                "semver": "bin/semver.js"
-            }
-        },
-        "node_modules/@mat3ra/utils/node_modules/@babel/helper-define-polyfill-provider": {
-            "version": "0.3.3",
-            "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.3.3.tgz",
-            "integrity": "sha512-z5aQKU4IzbqCC1XH0nAqfsFLMVSo22SBKUc0BxGrLkolTdPTructy0ToNnlO2zA4j9Q/7pjMZf0DSY+DSTYzww==",
-            "dev": true,
-            "license": "MIT",
-            "peer": true,
-            "dependencies": {
-                "@babel/helper-compilation-targets": "^7.17.7",
-                "@babel/helper-plugin-utils": "^7.16.7",
-                "debug": "^4.1.1",
-                "lodash.debounce": "^4.0.8",
-                "resolve": "^1.14.2",
-                "semver": "^6.1.2"
-            },
-            "peerDependencies": {
-                "@babel/core": "^7.4.0-0"
-            }
-        },
-        "node_modules/@mat3ra/utils/node_modules/@babel/helper-define-polyfill-provider/node_modules/semver": {
-            "version": "6.3.1",
-            "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
-            "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
-            "dev": true,
-            "license": "ISC",
-            "peer": true,
-            "bin": {
-                "semver": "bin/semver.js"
-            }
-        },
-        "node_modules/@mat3ra/utils/node_modules/@babel/plugin-proposal-class-properties": {
-            "version": "7.16.0",
-            "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.16.0.tgz",
-            "integrity": "sha512-mCF3HcuZSY9Fcx56Lbn+CGdT44ioBMMvjNVldpKtj8tpniETdLjnxdHI1+sDWXIM1nNt+EanJOZ3IG9lzVjs7A==",
-            "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-properties instead.",
-            "dev": true,
-            "license": "MIT",
-            "peer": true,
-            "dependencies": {
-                "@babel/helper-create-class-features-plugin": "^7.16.0",
-                "@babel/helper-plugin-utils": "^7.14.5"
-            },
-            "engines": {
-                "node": ">=6.9.0"
-            },
-            "peerDependencies": {
-                "@babel/core": "^7.0.0-0"
-            }
-        },
-        "node_modules/@mat3ra/utils/node_modules/@babel/plugin-proposal-private-property-in-object": {
-            "version": "7.21.11",
-            "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.11.tgz",
-            "integrity": "sha512-0QZ8qP/3RLDVBwBFoWAwCtgcDZJVwA5LUJRZU8x2YFfKNuFq161wK3cuGrALu5yiPu+vzwTAg/sMWVNeWeNyaw==",
-            "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-private-property-in-object instead.",
-            "dev": true,
-            "license": "MIT",
-            "peer": true,
-            "dependencies": {
-                "@babel/helper-annotate-as-pure": "^7.18.6",
-                "@babel/helper-create-class-features-plugin": "^7.21.0",
-                "@babel/helper-plugin-utils": "^7.20.2",
-                "@babel/plugin-syntax-private-property-in-object": "^7.14.5"
-            },
-            "engines": {
-                "node": ">=6.9.0"
-            },
-            "peerDependencies": {
-                "@babel/core": "^7.0.0-0"
-            }
-        },
-        "node_modules/@mat3ra/utils/node_modules/@babel/preset-env": {
-            "version": "7.16.4",
-            "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.16.4.tgz",
-            "integrity": "sha512-v0QtNd81v/xKj4gNKeuAerQ/azeNn/G1B1qMLeXOcV8+4TWlD2j3NV1u8q29SDFBXx/NBq5kyEAO+0mpRgacjA==",
-            "dev": true,
-            "license": "MIT",
-            "peer": true,
-            "dependencies": {
-                "@babel/compat-data": "^7.16.4",
-                "@babel/helper-compilation-targets": "^7.16.3",
-                "@babel/helper-plugin-utils": "^7.14.5",
-                "@babel/helper-validator-option": "^7.14.5",
-                "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.16.2",
-                "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.16.0",
-                "@babel/plugin-proposal-async-generator-functions": "^7.16.4",
-                "@babel/plugin-proposal-class-properties": "^7.16.0",
-                "@babel/plugin-proposal-class-static-block": "^7.16.0",
-                "@babel/plugin-proposal-dynamic-import": "^7.16.0",
-                "@babel/plugin-proposal-export-namespace-from": "^7.16.0",
-                "@babel/plugin-proposal-json-strings": "^7.16.0",
-                "@babel/plugin-proposal-logical-assignment-operators": "^7.16.0",
-                "@babel/plugin-proposal-nullish-coalescing-operator": "^7.16.0",
-                "@babel/plugin-proposal-numeric-separator": "^7.16.0",
-                "@babel/plugin-proposal-object-rest-spread": "^7.16.0",
-                "@babel/plugin-proposal-optional-catch-binding": "^7.16.0",
-                "@babel/plugin-proposal-optional-chaining": "^7.16.0",
-                "@babel/plugin-proposal-private-methods": "^7.16.0",
-                "@babel/plugin-proposal-private-property-in-object": "^7.16.0",
-                "@babel/plugin-proposal-unicode-property-regex": "^7.16.0",
-                "@babel/plugin-syntax-async-generators": "^7.8.4",
-                "@babel/plugin-syntax-class-properties": "^7.12.13",
-                "@babel/plugin-syntax-class-static-block": "^7.14.5",
-                "@babel/plugin-syntax-dynamic-import": "^7.8.3",
-                "@babel/plugin-syntax-export-namespace-from": "^7.8.3",
-                "@babel/plugin-syntax-json-strings": "^7.8.3",
-                "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4",
-                "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3",
-                "@babel/plugin-syntax-numeric-separator": "^7.10.4",
-                "@babel/plugin-syntax-object-rest-spread": "^7.8.3",
-                "@babel/plugin-syntax-optional-catch-binding": "^7.8.3",
-                "@babel/plugin-syntax-optional-chaining": "^7.8.3",
-                "@babel/plugin-syntax-private-property-in-object": "^7.14.5",
-                "@babel/plugin-syntax-top-level-await": "^7.14.5",
-                "@babel/plugin-transform-arrow-functions": "^7.16.0",
-                "@babel/plugin-transform-async-to-generator": "^7.16.0",
-                "@babel/plugin-transform-block-scoped-functions": "^7.16.0",
-                "@babel/plugin-transform-block-scoping": "^7.16.0",
-                "@babel/plugin-transform-classes": "^7.16.0",
-                "@babel/plugin-transform-computed-properties": "^7.16.0",
-                "@babel/plugin-transform-destructuring": "^7.16.0",
-                "@babel/plugin-transform-dotall-regex": "^7.16.0",
-                "@babel/plugin-transform-duplicate-keys": "^7.16.0",
-                "@babel/plugin-transform-exponentiation-operator": "^7.16.0",
-                "@babel/plugin-transform-for-of": "^7.16.0",
-                "@babel/plugin-transform-function-name": "^7.16.0",
-                "@babel/plugin-transform-literals": "^7.16.0",
-                "@babel/plugin-transform-member-expression-literals": "^7.16.0",
-                "@babel/plugin-transform-modules-amd": "^7.16.0",
-                "@babel/plugin-transform-modules-commonjs": "^7.16.0",
-                "@babel/plugin-transform-modules-systemjs": "^7.16.0",
-                "@babel/plugin-transform-modules-umd": "^7.16.0",
-                "@babel/plugin-transform-named-capturing-groups-regex": "^7.16.0",
-                "@babel/plugin-transform-new-target": "^7.16.0",
-                "@babel/plugin-transform-object-super": "^7.16.0",
-                "@babel/plugin-transform-parameters": "^7.16.3",
-                "@babel/plugin-transform-property-literals": "^7.16.0",
-                "@babel/plugin-transform-regenerator": "^7.16.0",
-                "@babel/plugin-transform-reserved-words": "^7.16.0",
-                "@babel/plugin-transform-shorthand-properties": "^7.16.0",
-                "@babel/plugin-transform-spread": "^7.16.0",
-                "@babel/plugin-transform-sticky-regex": "^7.16.0",
-                "@babel/plugin-transform-template-literals": "^7.16.0",
-                "@babel/plugin-transform-typeof-symbol": "^7.16.0",
-                "@babel/plugin-transform-unicode-escapes": "^7.16.0",
-                "@babel/plugin-transform-unicode-regex": "^7.16.0",
-                "@babel/preset-modules": "^0.1.5",
-                "@babel/types": "^7.16.0",
-                "babel-plugin-polyfill-corejs2": "^0.3.0",
-                "babel-plugin-polyfill-corejs3": "^0.4.0",
-                "babel-plugin-polyfill-regenerator": "^0.3.0",
-                "core-js-compat": "^3.19.1",
-                "semver": "^6.3.0"
-            },
-            "engines": {
-                "node": ">=6.9.0"
-            },
-            "peerDependencies": {
-                "@babel/core": "^7.0.0-0"
-            }
-        },
-        "node_modules/@mat3ra/utils/node_modules/@babel/preset-env/node_modules/semver": {
-            "version": "6.3.1",
-            "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
-            "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
-            "dev": true,
-            "license": "ISC",
-            "peer": true,
-            "bin": {
-                "semver": "bin/semver.js"
-            }
-        },
-        "node_modules/@mat3ra/utils/node_modules/@babel/preset-modules": {
-            "version": "0.1.6",
-            "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6.tgz",
-            "integrity": "sha512-ID2yj6K/4lKfhuU3+EX4UvNbIt7eACFbHmNUjzA+ep+B5971CknnA/9DEWKbRokfbbtblxxxXFJJrH47UEAMVg==",
-            "dev": true,
-            "license": "MIT",
-            "peer": true,
-            "dependencies": {
-                "@babel/helper-plugin-utils": "^7.0.0",
-                "@babel/plugin-proposal-unicode-property-regex": "^7.4.4",
-                "@babel/plugin-transform-dotall-regex": "^7.4.4",
-                "@babel/types": "^7.4.4",
-                "esutils": "^2.0.2"
-            },
-            "peerDependencies": {
-                "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0"
-            }
-        },
-        "node_modules/@mat3ra/utils/node_modules/@babel/preset-react": {
-            "version": "7.16.7",
-            "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.16.7.tgz",
-            "integrity": "sha512-fWpyI8UM/HE6DfPBzD8LnhQ/OcH8AgTaqcqP2nGOXEUV+VKBR5JRN9hCk9ai+zQQ57vtm9oWeXguBCPNUjytgA==",
-            "dev": true,
-            "license": "MIT",
-            "peer": true,
-            "dependencies": {
-                "@babel/helper-plugin-utils": "^7.16.7",
-                "@babel/helper-validator-option": "^7.16.7",
-                "@babel/plugin-transform-react-display-name": "^7.16.7",
-                "@babel/plugin-transform-react-jsx": "^7.16.7",
-                "@babel/plugin-transform-react-jsx-development": "^7.16.7",
-                "@babel/plugin-transform-react-pure-annotations": "^7.16.7"
-            },
-            "engines": {
-                "node": ">=6.9.0"
-            },
-            "peerDependencies": {
-                "@babel/core": "^7.0.0-0"
-            }
-        },
-        "node_modules/@mat3ra/utils/node_modules/@babel/runtime-corejs3": {
-            "version": "7.16.8",
-            "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.16.8.tgz",
-            "integrity": "sha512-3fKhuICS1lMz0plI5ktOE/yEtBRMVxplzRkdn6mJQ197XiY0JnrzYV0+Mxozq3JZ8SBV9Ecurmw1XsGbwOf+Sg==",
-            "dev": true,
-            "license": "MIT",
-            "peer": true,
-            "dependencies": {
-                "core-js-pure": "^3.20.2",
-                "regenerator-runtime": "^0.13.4"
-            },
-            "engines": {
-                "node": ">=6.9.0"
-            }
-        },
-        "node_modules/@mat3ra/utils/node_modules/babel-plugin-polyfill-corejs2": {
-            "version": "0.3.3",
-            "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.3.tgz",
-            "integrity": "sha512-8hOdmFYFSZhqg2C/JgLUQ+t52o5nirNwaWM2B9LWteozwIvM14VSwdsCAUET10qT+kmySAlseadmfeeSWFCy+Q==",
-            "dev": true,
-            "license": "MIT",
-            "peer": true,
-            "dependencies": {
-                "@babel/compat-data": "^7.17.7",
-                "@babel/helper-define-polyfill-provider": "^0.3.3",
-                "semver": "^6.1.1"
-            },
-            "peerDependencies": {
-                "@babel/core": "^7.0.0-0"
-            }
-        },
-        "node_modules/@mat3ra/utils/node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": {
-            "version": "6.3.1",
-            "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
-            "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
-            "dev": true,
-            "license": "ISC",
-            "peer": true,
-            "bin": {
-                "semver": "bin/semver.js"
-            }
-        },
-        "node_modules/@mat3ra/utils/node_modules/babel-plugin-polyfill-corejs3": {
-            "version": "0.4.0",
-            "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.4.0.tgz",
-            "integrity": "sha512-YxFreYwUfglYKdLUGvIF2nJEsGwj+RhWSX/ije3D2vQPOXuyMLMtg/cCGMDpOA7Nd+MwlNdnGODbd2EwUZPlsw==",
-            "dev": true,
-            "license": "MIT",
-            "peer": true,
-            "dependencies": {
-                "@babel/helper-define-polyfill-provider": "^0.3.0",
-                "core-js-compat": "^3.18.0"
-            },
-            "peerDependencies": {
-                "@babel/core": "^7.0.0-0"
-            }
-        },
-        "node_modules/@mat3ra/utils/node_modules/babel-plugin-polyfill-regenerator": {
-            "version": "0.3.1",
-            "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.3.1.tgz",
-            "integrity": "sha512-Y2B06tvgHYt1x0yz17jGkGeeMr5FeKUu+ASJ+N6nB5lQ8Dapfg42i0OVrf8PNGJ3zKL4A23snMi1IRwrqqND7A==",
-            "dev": true,
-            "license": "MIT",
-            "peer": true,
-            "dependencies": {
-                "@babel/helper-define-polyfill-provider": "^0.3.1"
-            },
-            "peerDependencies": {
-                "@babel/core": "^7.0.0-0"
-            }
-        },
-        "node_modules/@mat3ra/utils/node_modules/commander": {
-            "version": "4.1.1",
-            "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
-            "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
-            "dev": true,
-            "license": "MIT",
-            "peer": true,
-            "engines": {
-                "node": ">= 6"
-            }
-        },
-        "node_modules/@mat3ra/utils/node_modules/semver": {
-            "version": "7.8.5",
-            "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
-            "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
-            "dev": true,
-            "license": "ISC",
-            "peer": true,
-            "bin": {
-                "semver": "bin/semver.js"
-            },
-            "engines": {
-                "node": ">=10"
+                "node": ">=10"
             }
         },
         "node_modules/@mat3ra/utils/node_modules/typescript": {
@@ -6203,7 +5227,6 @@
             "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
             "dev": true,
             "license": "Apache-2.0",
-            "peer": true,
             "bin": {
                 "tsc": "bin/tsc",
                 "tsserver": "bin/tsserver"
@@ -7016,47 +6039,6 @@
                 "eslint-scope": "5.1.1"
             }
         },
-        "node_modules/@nodelib/fs.scandir": {
-            "version": "2.1.5",
-            "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
-            "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
-            "dev": true,
-            "license": "MIT",
-            "peer": true,
-            "dependencies": {
-                "@nodelib/fs.stat": "2.0.5",
-                "run-parallel": "^1.1.9"
-            },
-            "engines": {
-                "node": ">= 8"
-            }
-        },
-        "node_modules/@nodelib/fs.stat": {
-            "version": "2.0.5",
-            "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
-            "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
-            "dev": true,
-            "license": "MIT",
-            "peer": true,
-            "engines": {
-                "node": ">= 8"
-            }
-        },
-        "node_modules/@nodelib/fs.walk": {
-            "version": "1.2.8",
-            "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
-            "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
-            "dev": true,
-            "license": "MIT",
-            "peer": true,
-            "dependencies": {
-                "@nodelib/fs.scandir": "2.1.5",
-                "fastq": "^1.6.0"
-            },
-            "engines": {
-                "node": ">= 8"
-            }
-        },
         "node_modules/@popperjs/core": {
             "version": "2.11.8",
             "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
@@ -7979,14 +6961,6 @@
                 "react-dom": ">=17.0.0"
             }
         },
-        "node_modules/@ungap/structured-clone": {
-            "version": "1.3.3",
-            "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.3.tgz",
-            "integrity": "sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==",
-            "dev": true,
-            "license": "ISC",
-            "peer": true
-        },
         "node_modules/@vitejs/plugin-react": {
             "version": "4.7.0",
             "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
@@ -8024,19 +6998,8 @@
             "bin": {
                 "acorn": "bin/acorn"
             },
-            "engines": {
-                "node": ">=0.4.0"
-            }
-        },
-        "node_modules/acorn-jsx": {
-            "version": "5.3.2",
-            "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
-            "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
-            "dev": true,
-            "license": "MIT",
-            "peer": true,
-            "peerDependencies": {
-                "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+            "engines": {
+                "node": ">=0.4.0"
             }
         },
         "node_modules/acorn-walk": {
@@ -8127,23 +7090,6 @@
                 "url": "https://github.com/chalk/ansi-regex?sponsor=1"
             }
         },
-        "node_modules/ansi-styles": {
-            "version": "4.3.0",
-            "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
-            "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
-            "dev": true,
-            "license": "MIT",
-            "peer": true,
-            "dependencies": {
-                "color-convert": "^2.0.1"
-            },
-            "engines": {
-                "node": ">=8"
-            },
-            "funding": {
-                "url": "https://github.com/chalk/ansi-styles?sponsor=1"
-            }
-        },
         "node_modules/anymatch": {
             "version": "3.1.3",
             "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
@@ -8959,28 +7905,6 @@
                 "@codemirror/view": "^6.0.0"
             }
         },
-        "node_modules/color-convert": {
-            "version": "2.0.1",
-            "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
-            "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
-            "dev": true,
-            "license": "MIT",
-            "peer": true,
-            "dependencies": {
-                "color-name": "~1.1.4"
-            },
-            "engines": {
-                "node": ">=7.0.0"
-            }
-        },
-        "node_modules/color-name": {
-            "version": "1.1.4",
-            "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
-            "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
-            "dev": true,
-            "license": "MIT",
-            "peer": true
-        },
         "node_modules/commander": {
             "version": "6.2.1",
             "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz",
@@ -9200,22 +8124,6 @@
                 "node-fetch": "^2.7.0"
             }
         },
-        "node_modules/cross-spawn": {
-            "version": "7.0.6",
-            "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
-            "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
-            "dev": true,
-            "license": "MIT",
-            "peer": true,
-            "dependencies": {
-                "path-key": "^3.1.0",
-                "shebang-command": "^2.0.0",
-                "which": "^2.0.1"
-            },
-            "engines": {
-                "node": ">= 8"
-            }
-        },
         "node_modules/crypt": {
             "version": "0.0.2",
             "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz",
@@ -9387,14 +8295,6 @@
                 "node": ">=6"
             }
         },
-        "node_modules/deep-is": {
-            "version": "0.1.4",
-            "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
-            "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
-            "dev": true,
-            "license": "MIT",
-            "peer": true
-        },
         "node_modules/define-data-property": {
             "version": "1.1.4",
             "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
@@ -9478,20 +8378,6 @@
             "dev": true,
             "license": "MIT"
         },
-        "node_modules/doctrine": {
-            "version": "3.0.0",
-            "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
-            "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
-            "dev": true,
-            "license": "Apache-2.0",
-            "peer": true,
-            "dependencies": {
-                "esutils": "^2.0.2"
-            },
-            "engines": {
-                "node": ">=6.0.0"
-            }
-        },
         "node_modules/dom-helpers": {
             "version": "5.2.1",
             "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
@@ -9808,64 +8694,6 @@
                 "url": "https://github.com/sponsors/sindresorhus"
             }
         },
-        "node_modules/eslint": {
-            "version": "8.57.1",
-            "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz",
-            "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==",
-            "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
-            "dev": true,
-            "license": "MIT",
-            "peer": true,
-            "dependencies": {
-                "@eslint-community/eslint-utils": "^4.2.0",
-                "@eslint-community/regexpp": "^4.6.1",
-                "@eslint/eslintrc": "^2.1.4",
-                "@eslint/js": "8.57.1",
-                "@humanwhocodes/config-array": "^0.13.0",
-                "@humanwhocodes/module-importer": "^1.0.1",
-                "@nodelib/fs.walk": "^1.2.8",
-                "@ungap/structured-clone": "^1.2.0",
-                "ajv": "^6.12.4",
-                "chalk": "^4.0.0",
-                "cross-spawn": "^7.0.2",
-                "debug": "^4.3.2",
-                "doctrine": "^3.0.0",
-                "escape-string-regexp": "^4.0.0",
-                "eslint-scope": "^7.2.2",
-                "eslint-visitor-keys": "^3.4.3",
-                "espree": "^9.6.1",
-                "esquery": "^1.4.2",
-                "esutils": "^2.0.2",
-                "fast-deep-equal": "^3.1.3",
-                "file-entry-cache": "^6.0.1",
-                "find-up": "^5.0.0",
-                "glob-parent": "^6.0.2",
-                "globals": "^13.19.0",
-                "graphemer": "^1.4.0",
-                "ignore": "^5.2.0",
-                "imurmurhash": "^0.1.4",
-                "is-glob": "^4.0.0",
-                "is-path-inside": "^3.0.3",
-                "js-yaml": "^4.1.0",
-                "json-stable-stringify-without-jsonify": "^1.0.1",
-                "levn": "^0.4.1",
-                "lodash.merge": "^4.6.2",
-                "minimatch": "^3.1.2",
-                "natural-compare": "^1.4.0",
-                "optionator": "^0.9.3",
-                "strip-ansi": "^6.0.1",
-                "text-table": "^0.2.0"
-            },
-            "bin": {
-                "eslint": "bin/eslint.js"
-            },
-            "engines": {
-                "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
-            },
-            "funding": {
-                "url": "https://opencollective.com/eslint"
-            }
-        },
         "node_modules/eslint-scope": {
             "version": "5.1.1",
             "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
@@ -9890,270 +8718,6 @@
                 "node": ">=10"
             }
         },
-        "node_modules/eslint/node_modules/ajv": {
-            "version": "6.15.0",
-            "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz",
-            "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==",
-            "dev": true,
-            "license": "MIT",
-            "peer": true,
-            "dependencies": {
-                "fast-deep-equal": "^3.1.1",
-                "fast-json-stable-stringify": "^2.0.0",
-                "json-schema-traverse": "^0.4.1",
-                "uri-js": "^4.2.2"
-            },
-            "funding": {
-                "type": "github",
-                "url": "https://github.com/sponsors/epoberezkin"
-            }
-        },
-        "node_modules/eslint/node_modules/ansi-regex": {
-            "version": "5.0.1",
-            "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
-            "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
-            "dev": true,
-            "license": "MIT",
-            "peer": true,
-            "engines": {
-                "node": ">=8"
-            }
-        },
-        "node_modules/eslint/node_modules/chalk": {
-            "version": "4.1.2",
-            "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
-            "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
-            "dev": true,
-            "license": "MIT",
-            "peer": true,
-            "dependencies": {
-                "ansi-styles": "^4.1.0",
-                "supports-color": "^7.1.0"
-            },
-            "engines": {
-                "node": ">=10"
-            },
-            "funding": {
-                "url": "https://github.com/chalk/chalk?sponsor=1"
-            }
-        },
-        "node_modules/eslint/node_modules/eslint-scope": {
-            "version": "7.2.2",
-            "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
-            "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==",
-            "dev": true,
-            "license": "BSD-2-Clause",
-            "peer": true,
-            "dependencies": {
-                "esrecurse": "^4.3.0",
-                "estraverse": "^5.2.0"
-            },
-            "engines": {
-                "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
-            },
-            "funding": {
-                "url": "https://opencollective.com/eslint"
-            }
-        },
-        "node_modules/eslint/node_modules/eslint-visitor-keys": {
-            "version": "3.4.3",
-            "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
-            "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
-            "dev": true,
-            "license": "Apache-2.0",
-            "peer": true,
-            "engines": {
-                "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
-            },
-            "funding": {
-                "url": "https://opencollective.com/eslint"
-            }
-        },
-        "node_modules/eslint/node_modules/estraverse": {
-            "version": "5.3.0",
-            "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
-            "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
-            "dev": true,
-            "license": "BSD-2-Clause",
-            "peer": true,
-            "engines": {
-                "node": ">=4.0"
-            }
-        },
-        "node_modules/eslint/node_modules/find-up": {
-            "version": "5.0.0",
-            "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
-            "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
-            "dev": true,
-            "license": "MIT",
-            "peer": true,
-            "dependencies": {
-                "locate-path": "^6.0.0",
-                "path-exists": "^4.0.0"
-            },
-            "engines": {
-                "node": ">=10"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/sindresorhus"
-            }
-        },
-        "node_modules/eslint/node_modules/glob-parent": {
-            "version": "6.0.2",
-            "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
-            "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
-            "dev": true,
-            "license": "ISC",
-            "peer": true,
-            "dependencies": {
-                "is-glob": "^4.0.3"
-            },
-            "engines": {
-                "node": ">=10.13.0"
-            }
-        },
-        "node_modules/eslint/node_modules/json-schema-traverse": {
-            "version": "0.4.1",
-            "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
-            "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
-            "dev": true,
-            "license": "MIT",
-            "peer": true
-        },
-        "node_modules/eslint/node_modules/locate-path": {
-            "version": "6.0.0",
-            "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
-            "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
-            "dev": true,
-            "license": "MIT",
-            "peer": true,
-            "dependencies": {
-                "p-locate": "^5.0.0"
-            },
-            "engines": {
-                "node": ">=10"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/sindresorhus"
-            }
-        },
-        "node_modules/eslint/node_modules/p-limit": {
-            "version": "3.1.0",
-            "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
-            "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
-            "dev": true,
-            "license": "MIT",
-            "peer": true,
-            "dependencies": {
-                "yocto-queue": "^0.1.0"
-            },
-            "engines": {
-                "node": ">=10"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/sindresorhus"
-            }
-        },
-        "node_modules/eslint/node_modules/p-locate": {
-            "version": "5.0.0",
-            "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
-            "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
-            "dev": true,
-            "license": "MIT",
-            "peer": true,
-            "dependencies": {
-                "p-limit": "^3.0.2"
-            },
-            "engines": {
-                "node": ">=10"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/sindresorhus"
-            }
-        },
-        "node_modules/eslint/node_modules/path-exists": {
-            "version": "4.0.0",
-            "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
-            "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
-            "dev": true,
-            "license": "MIT",
-            "peer": true,
-            "engines": {
-                "node": ">=8"
-            }
-        },
-        "node_modules/eslint/node_modules/strip-ansi": {
-            "version": "6.0.1",
-            "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
-            "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
-            "dev": true,
-            "license": "MIT",
-            "peer": true,
-            "dependencies": {
-                "ansi-regex": "^5.0.1"
-            },
-            "engines": {
-                "node": ">=8"
-            }
-        },
-        "node_modules/espree": {
-            "version": "9.6.1",
-            "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
-            "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==",
-            "dev": true,
-            "license": "BSD-2-Clause",
-            "peer": true,
-            "dependencies": {
-                "acorn": "^8.9.0",
-                "acorn-jsx": "^5.3.2",
-                "eslint-visitor-keys": "^3.4.1"
-            },
-            "engines": {
-                "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
-            },
-            "funding": {
-                "url": "https://opencollective.com/eslint"
-            }
-        },
-        "node_modules/espree/node_modules/eslint-visitor-keys": {
-            "version": "3.4.3",
-            "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
-            "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
-            "dev": true,
-            "license": "Apache-2.0",
-            "peer": true,
-            "engines": {
-                "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
-            },
-            "funding": {
-                "url": "https://opencollective.com/eslint"
-            }
-        },
-        "node_modules/esquery": {
-            "version": "1.7.0",
-            "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
-            "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
-            "dev": true,
-            "license": "BSD-3-Clause",
-            "peer": true,
-            "dependencies": {
-                "estraverse": "^5.1.0"
-            },
-            "engines": {
-                "node": ">=0.10"
-            }
-        },
-        "node_modules/esquery/node_modules/estraverse": {
-            "version": "5.3.0",
-            "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
-            "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
-            "dev": true,
-            "license": "BSD-2-Clause",
-            "peer": true,
-            "engines": {
-                "node": ">=4.0"
-            }
-        },
         "node_modules/esrecurse": {
             "version": "4.3.0",
             "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
@@ -10232,22 +8796,6 @@
             "dev": true,
             "license": "MIT"
         },
-        "node_modules/fast-json-stable-stringify": {
-            "version": "2.1.0",
-            "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
-            "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
-            "dev": true,
-            "license": "MIT",
-            "peer": true
-        },
-        "node_modules/fast-levenshtein": {
-            "version": "2.0.6",
-            "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
-            "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
-            "dev": true,
-            "license": "MIT",
-            "peer": true
-        },
         "node_modules/fast-uri": {
             "version": "3.1.3",
             "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.3.tgz",
@@ -10265,17 +8813,6 @@
             ],
             "license": "BSD-3-Clause"
         },
-        "node_modules/fastq": {
-            "version": "1.20.1",
-            "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
-            "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==",
-            "dev": true,
-            "license": "ISC",
-            "peer": true,
-            "dependencies": {
-                "reusify": "^1.0.4"
-            }
-        },
         "node_modules/fbemitter": {
             "version": "3.0.0",
             "resolved": "https://registry.npmjs.org/fbemitter/-/fbemitter-3.0.0.tgz",
@@ -10321,24 +8858,10 @@
             "peerDependencies": {
                 "picomatch": "^3 || ^4"
             },
-            "peerDependenciesMeta": {
-                "picomatch": {
-                    "optional": true
-                }
-            }
-        },
-        "node_modules/file-entry-cache": {
-            "version": "6.0.1",
-            "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
-            "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==",
-            "dev": true,
-            "license": "MIT",
-            "peer": true,
-            "dependencies": {
-                "flat-cache": "^3.0.4"
-            },
-            "engines": {
-                "node": "^10.12.0 || >=12.0.0"
+            "peerDependenciesMeta": {
+                "picomatch": {
+                    "optional": true
+                }
             }
         },
         "node_modules/fill-range": {
@@ -10390,30 +8913,6 @@
                 "node": ">=6"
             }
         },
-        "node_modules/flat-cache": {
-            "version": "3.2.0",
-            "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz",
-            "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==",
-            "dev": true,
-            "license": "MIT",
-            "peer": true,
-            "dependencies": {
-                "flatted": "^3.2.9",
-                "keyv": "^4.5.3",
-                "rimraf": "^3.0.2"
-            },
-            "engines": {
-                "node": "^10.12.0 || >=12.0.0"
-            }
-        },
-        "node_modules/flatted": {
-            "version": "3.4.4",
-            "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz",
-            "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==",
-            "dev": true,
-            "license": "ISC",
-            "peer": true
-        },
         "node_modules/flux": {
             "version": "4.0.4",
             "resolved": "https://registry.npmjs.org/flux/-/flux-4.0.4.tgz",
@@ -10668,23 +9167,6 @@
                 "node": ">= 6"
             }
         },
-        "node_modules/globals": {
-            "version": "13.24.0",
-            "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz",
-            "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==",
-            "dev": true,
-            "license": "MIT",
-            "peer": true,
-            "dependencies": {
-                "type-fest": "^0.20.2"
-            },
-            "engines": {
-                "node": ">=8"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/sindresorhus"
-            }
-        },
         "node_modules/globalthis": {
             "version": "1.0.4",
             "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz",
@@ -10725,14 +9207,6 @@
                 "url": "https://github.com/sponsors/ljharb"
             }
         },
-        "node_modules/graphemer": {
-            "version": "1.4.0",
-            "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
-            "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
-            "dev": true,
-            "license": "MIT",
-            "peer": true
-        },
         "node_modules/has-bigints": {
             "version": "1.1.0",
             "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz",
@@ -10746,17 +9220,6 @@
                 "url": "https://github.com/sponsors/ljharb"
             }
         },
-        "node_modules/has-flag": {
-            "version": "4.0.0",
-            "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
-            "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
-            "dev": true,
-            "license": "MIT",
-            "peer": true,
-            "engines": {
-                "node": ">=8"
-            }
-        },
         "node_modules/has-property-descriptors": {
             "version": "1.0.2",
             "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
@@ -10938,17 +9401,6 @@
             ],
             "license": "BSD-3-Clause"
         },
-        "node_modules/ignore": {
-            "version": "5.3.2",
-            "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
-            "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
-            "dev": true,
-            "license": "MIT",
-            "peer": true,
-            "engines": {
-                "node": ">= 4"
-            }
-        },
         "node_modules/immediate": {
             "version": "3.0.6",
             "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
@@ -10973,17 +9425,6 @@
                 "url": "https://github.com/sponsors/sindresorhus"
             }
         },
-        "node_modules/imurmurhash": {
-            "version": "0.1.4",
-            "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
-            "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
-            "dev": true,
-            "license": "MIT",
-            "peer": true,
-            "engines": {
-                "node": ">=0.8.19"
-            }
-        },
         "node_modules/inflight": {
             "version": "1.0.6",
             "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
@@ -11387,17 +9828,6 @@
                 "url": "https://github.com/sponsors/ljharb"
             }
         },
-        "node_modules/is-path-inside": {
-            "version": "3.0.3",
-            "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
-            "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==",
-            "dev": true,
-            "license": "MIT",
-            "peer": true,
-            "engines": {
-                "node": ">=8"
-            }
-        },
         "node_modules/is-plain-object": {
             "version": "2.0.4",
             "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
@@ -11576,14 +10006,6 @@
             "dev": true,
             "license": "MIT"
         },
-        "node_modules/isexe": {
-            "version": "2.0.0",
-            "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
-            "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
-            "dev": true,
-            "license": "ISC",
-            "peer": true
-        },
         "node_modules/isobject": {
             "version": "3.0.1",
             "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
@@ -11654,14 +10076,6 @@
                 "node": ">=6"
             }
         },
-        "node_modules/json-buffer": {
-            "version": "3.0.1",
-            "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
-            "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
-            "dev": true,
-            "license": "MIT",
-            "peer": true
-        },
         "node_modules/json-parse-even-better-errors": {
             "version": "2.3.1",
             "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
@@ -11762,14 +10176,6 @@
             "dev": true,
             "license": "MIT"
         },
-        "node_modules/json-stable-stringify-without-jsonify": {
-            "version": "1.0.1",
-            "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
-            "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
-            "dev": true,
-            "license": "MIT",
-            "peer": true
-        },
         "node_modules/json5": {
             "version": "2.2.3",
             "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
@@ -11938,17 +10344,6 @@
                 "node": ">= 12"
             }
         },
-        "node_modules/keyv": {
-            "version": "4.5.4",
-            "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
-            "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
-            "dev": true,
-            "license": "MIT",
-            "peer": true,
-            "dependencies": {
-                "json-buffer": "3.0.1"
-            }
-        },
         "node_modules/kind-of": {
             "version": "6.0.3",
             "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
@@ -11959,21 +10354,6 @@
                 "node": ">=0.10.0"
             }
         },
-        "node_modules/levn": {
-            "version": "0.4.1",
-            "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
-            "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
-            "dev": true,
-            "license": "MIT",
-            "peer": true,
-            "dependencies": {
-                "prelude-ls": "^1.2.1",
-                "type-check": "~0.4.0"
-            },
-            "engines": {
-                "node": ">= 0.8.0"
-            }
-        },
         "node_modules/lie": {
             "version": "3.3.0",
             "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
@@ -12040,14 +10420,6 @@
             "dev": true,
             "license": "MIT"
         },
-        "node_modules/lodash.merge": {
-            "version": "4.6.2",
-            "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
-            "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
-            "dev": true,
-            "license": "MIT",
-            "peer": true
-        },
         "node_modules/loose-envify": {
             "version": "1.4.0",
             "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
@@ -12313,14 +10685,6 @@
                 "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
             }
         },
-        "node_modules/natural-compare": {
-            "version": "1.4.0",
-            "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
-            "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
-            "dev": true,
-            "license": "MIT",
-            "peer": true
-        },
         "node_modules/node-fetch": {
             "version": "2.7.0",
             "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
@@ -12655,25 +11019,6 @@
                 "wrappy": "1"
             }
         },
-        "node_modules/optionator": {
-            "version": "0.9.4",
-            "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
-            "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
-            "dev": true,
-            "license": "MIT",
-            "peer": true,
-            "dependencies": {
-                "deep-is": "^0.1.3",
-                "fast-levenshtein": "^2.0.6",
-                "levn": "^0.4.1",
-                "prelude-ls": "^1.2.1",
-                "type-check": "^0.4.0",
-                "word-wrap": "^1.2.5"
-            },
-            "engines": {
-                "node": ">= 0.8.0"
-            }
-        },
         "node_modules/orderedmap": {
             "version": "2.1.1",
             "resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-2.1.1.tgz",
@@ -12849,17 +11194,6 @@
                 "node": ">=0.10.0"
             }
         },
-        "node_modules/path-key": {
-            "version": "3.1.1",
-            "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
-            "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
-            "dev": true,
-            "license": "MIT",
-            "peer": true,
-            "engines": {
-                "node": ">=8"
-            }
-        },
         "node_modules/path-parse": {
             "version": "1.0.7",
             "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
@@ -13025,17 +11359,6 @@
                 "node": "^10 || ^12 || >=14"
             }
         },
-        "node_modules/prelude-ls": {
-            "version": "1.2.1",
-            "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
-            "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
-            "dev": true,
-            "license": "MIT",
-            "peer": true,
-            "engines": {
-                "node": ">= 0.8.0"
-            }
-        },
         "node_modules/prettier": {
             "version": "3.9.4",
             "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.4.tgz",
@@ -13261,28 +11584,6 @@
                 "node": ">=0.4.x"
             }
         },
-        "node_modules/queue-microtask": {
-            "version": "1.2.3",
-            "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
-            "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
-            "dev": true,
-            "funding": [
-                {
-                    "type": "github",
-                    "url": "https://github.com/sponsors/feross"
-                },
-                {
-                    "type": "patreon",
-                    "url": "https://www.patreon.com/feross"
-                },
-                {
-                    "type": "consulting",
-                    "url": "https://feross.org/support"
-                }
-            ],
-            "license": "MIT",
-            "peer": true
-        },
         "node_modules/raf-schd": {
             "version": "4.0.3",
             "resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz",
@@ -13719,36 +12020,6 @@
                 "node": ">=4"
             }
         },
-        "node_modules/reusify": {
-            "version": "1.1.0",
-            "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
-            "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
-            "dev": true,
-            "license": "MIT",
-            "peer": true,
-            "engines": {
-                "iojs": ">=1.0.0",
-                "node": ">=0.10.0"
-            }
-        },
-        "node_modules/rimraf": {
-            "version": "3.0.2",
-            "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
-            "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
-            "deprecated": "Rimraf versions prior to v4 are no longer supported",
-            "dev": true,
-            "license": "ISC",
-            "peer": true,
-            "dependencies": {
-                "glob": "^7.1.3"
-            },
-            "bin": {
-                "rimraf": "bin.js"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/isaacs"
-            }
-        },
         "node_modules/ripemd160": {
             "version": "2.0.3",
             "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.3.tgz",
@@ -13852,31 +12123,6 @@
             "dev": true,
             "license": "MIT"
         },
-        "node_modules/run-parallel": {
-            "version": "1.2.0",
-            "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
-            "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
-            "dev": true,
-            "funding": [
-                {
-                    "type": "github",
-                    "url": "https://github.com/sponsors/feross"
-                },
-                {
-                    "type": "patreon",
-                    "url": "https://www.patreon.com/feross"
-                },
-                {
-                    "type": "consulting",
-                    "url": "https://feross.org/support"
-                }
-            ],
-            "license": "MIT",
-            "peer": true,
-            "dependencies": {
-                "queue-microtask": "^1.2.2"
-            }
-        },
         "node_modules/safe-array-concat": {
             "version": "1.1.4",
             "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.4.tgz",
@@ -14099,31 +12345,6 @@
                 "node": ">=8"
             }
         },
-        "node_modules/shebang-command": {
-            "version": "2.0.0",
-            "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
-            "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
-            "dev": true,
-            "license": "MIT",
-            "peer": true,
-            "dependencies": {
-                "shebang-regex": "^3.0.0"
-            },
-            "engines": {
-                "node": ">=8"
-            }
-        },
-        "node_modules/shebang-regex": {
-            "version": "3.0.0",
-            "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
-            "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
-            "dev": true,
-            "license": "MIT",
-            "peer": true,
-            "engines": {
-                "node": ">=8"
-            }
-        },
         "node_modules/side-channel": {
             "version": "1.1.1",
             "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz",
@@ -14412,20 +12633,6 @@
                 "url": "https://github.com/chalk/strip-ansi?sponsor=1"
             }
         },
-        "node_modules/strip-json-comments": {
-            "version": "3.1.1",
-            "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
-            "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
-            "dev": true,
-            "license": "MIT",
-            "peer": true,
-            "engines": {
-                "node": ">=8"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/sindresorhus"
-            }
-        },
         "node_modules/style-mod": {
             "version": "4.1.3",
             "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz",
@@ -14440,20 +12647,6 @@
             "dev": true,
             "license": "MIT"
         },
-        "node_modules/supports-color": {
-            "version": "7.2.0",
-            "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
-            "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
-            "dev": true,
-            "license": "MIT",
-            "peer": true,
-            "dependencies": {
-                "has-flag": "^4.0.0"
-            },
-            "engines": {
-                "node": ">=8"
-            }
-        },
         "node_modules/supports-preserve-symlinks-flag": {
             "version": "1.0.0",
             "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
@@ -14467,14 +12660,6 @@
                 "url": "https://github.com/sponsors/ljharb"
             }
         },
-        "node_modules/text-table": {
-            "version": "0.2.0",
-            "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
-            "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
-            "dev": true,
-            "license": "MIT",
-            "peer": true
-        },
         "node_modules/timers-browserify": {
             "version": "2.0.12",
             "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.12.tgz",
@@ -14678,20 +12863,6 @@
             "dev": true,
             "license": "MIT"
         },
-        "node_modules/type-check": {
-            "version": "0.4.0",
-            "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
-            "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
-            "dev": true,
-            "license": "MIT",
-            "peer": true,
-            "dependencies": {
-                "prelude-ls": "^1.2.1"
-            },
-            "engines": {
-                "node": ">= 0.8.0"
-            }
-        },
         "node_modules/type-detect": {
             "version": "4.1.0",
             "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz",
@@ -14702,20 +12873,6 @@
                 "node": ">=4"
             }
         },
-        "node_modules/type-fest": {
-            "version": "0.20.2",
-            "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
-            "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
-            "dev": true,
-            "license": "(MIT OR CC0-1.0)",
-            "peer": true,
-            "engines": {
-                "node": ">=10"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/sindresorhus"
-            }
-        },
         "node_modules/typed-array-buffer": {
             "version": "1.0.3",
             "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz",
@@ -14990,28 +13147,6 @@
                 "browserslist": ">= 4.21.0"
             }
         },
-        "node_modules/uri-js": {
-            "version": "4.4.1",
-            "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
-            "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
-            "dev": true,
-            "license": "BSD-2-Clause",
-            "peer": true,
-            "dependencies": {
-                "punycode": "^2.1.0"
-            }
-        },
-        "node_modules/uri-js/node_modules/punycode": {
-            "version": "2.3.1",
-            "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
-            "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
-            "dev": true,
-            "license": "MIT",
-            "peer": true,
-            "engines": {
-                "node": ">=6"
-            }
-        },
         "node_modules/url": {
             "version": "0.11.4",
             "resolved": "https://registry.npmjs.org/url/-/url-0.11.4.tgz",
@@ -15775,23 +13910,6 @@
                 "webidl-conversions": "^3.0.0"
             }
         },
-        "node_modules/which": {
-            "version": "2.0.2",
-            "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
-            "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
-            "dev": true,
-            "license": "ISC",
-            "peer": true,
-            "dependencies": {
-                "isexe": "^2.0.0"
-            },
-            "bin": {
-                "node-which": "bin/node-which"
-            },
-            "engines": {
-                "node": ">= 8"
-            }
-        },
         "node_modules/which-boxed-primitive": {
             "version": "1.1.1",
             "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz",
@@ -15888,17 +14006,6 @@
                 "url": "https://github.com/sponsors/ljharb"
             }
         },
-        "node_modules/word-wrap": {
-            "version": "1.2.5",
-            "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
-            "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
-            "dev": true,
-            "license": "MIT",
-            "peer": true,
-            "engines": {
-                "node": ">=0.10.0"
-            }
-        },
         "node_modules/wrappy": {
             "version": "1.0.2",
             "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
@@ -15923,24 +14030,6 @@
             "dev": true,
             "license": "ISC"
         },
-        "node_modules/yaml": {
-            "version": "2.9.0",
-            "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz",
-            "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==",
-            "dev": true,
-            "license": "ISC",
-            "optional": true,
-            "peer": true,
-            "bin": {
-                "yaml": "bin.mjs"
-            },
-            "engines": {
-                "node": ">= 14.6"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/eemeli"
-            }
-        },
         "node_modules/yn": {
             "version": "3.1.1",
             "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
diff --git a/package.json b/package.json
index 0946859..9006c20 100644
--- a/package.json
+++ b/package.json
@@ -49,9 +49,9 @@
         "@mat3ra/ade": "2026.8.18-0",
         "@mat3ra/code": "2026.8.18-0",
         "@mat3ra/esse": "2026.8.18-2",
-        "@mat3ra/ide": "https://github.com/mat3ra/ide/releases/download/wip-88b8881/ide.tgz",
-        "@mat3ra/made": "https://github.com/mat3ra/made/releases/download/wip-b5ab8c9/made.tgz",
-        "@mat3ra/mode": "https://github.com/mat3ra/mode/releases/download/wip-f8543e7/mode.tgz",
+        "@mat3ra/ide": "2026.8.18-0",
+        "@mat3ra/made": "2026.8.18-0",
+        "@mat3ra/mode": "2026.8.18-0",
         "@mat3ra/prode": "2026.8.18-0",
         "@mat3ra/standata": "2026.8.18-0",
         "@mat3ra/tsconfig": "^2024.6.3-0",