Engineering Journal
Schema Editor
Schema Editor

Your visual editor needs a linter: rule engines over connectivity graphs

2026-07-17

TLDR

While IDEs lint code continuously, visual editors rarely provide semantic linting, even when diagrams encode machine-checkable domain logic. Once a canvas application constructs a queryable graph model (components, wires, and transitive nets), adding a modular rule engine transforms static drawings into verified engineering assets. Isolating individual rule checks within try/catch guards and linking findings directly to canvas elements creates a powerful linting workflow.
Rule Engine LayerResponsibilityKey Output / Benefit
Transitive Net GraphUnion-Find connectivity computation$O(1)$ net membership queries
Modular Rule EngineEvaluates pure check(ctx) functionsIsolated finding collection (error, warning)
Interactive Findings PanelDisplays finding counts & canvas linksDirect visual element highlighting

Problem statement: the limits of visual inspection

In diagram editors (electrical schematics, UML architecture, database ERDs, workflow graphs), users frequently introduce semantic errors that are visually subtle but syntactically broken:

Without an automated rule engine, finding these defects requires manual visual inspection.


Technical architecture: queryable models & pure rule checkers

1. The prerequisite queryable context

A canvas linter requires a queryable context (ctx) containing components, wires, and transitively computed nets:
// Queryable Linter Context Snapshot
const linterContext = {
  components: getComponents(), // Component nodes with spec references
  wires: getWires(),           // Edge segments with resolved endpoints
  nets: computeNetsUnionFind() // Transitive net sets (Union-Find)
};

2. Pure rule declaration shape

Each rule is defined as metadata plus a pure check function that returns element-linked findings:
// Modular Rule Definition: Shorted Net Check
const powerShortRule = {
  id: 'power-short-check',
  severity: 'error',
  check(ctx) {
    const findings = [];

ctx.nets.forEach(net => { const symbols = Array.from(net.componentIds).map(id => getSymbolType(id)); const hasPower = symbols.some(s => s === 'VCC' || s === 'VDD'); const hasGround = symbols.some(s => s === 'GND');

if (hasPower && hasGround) { findings.push({ message: Critical Short Circuit: Net '${net.id}' connects Power and Ground., elementIds: Array.from(net.wireIds) }); } });

return findings; } };

3. Crash-Isolated rule runner

Run rules inside try/catch wrappers to ensure a single failing rule cannot crash the entire linting pipeline:
// Crash-Isolated Linter Execution
function runLinter(rules, ctx) {
  const allFindings = [];

rules.forEach(rule => { try { const ruleFindings = rule.check(ctx); ruleFindings.forEach(f => { allFindings.push({ ...f, ruleId: rule.id, severity: rule.severity }); }); } catch (err) { console.error(Rule '${rule.id}' threw an error:, err); } });

return allFindings; }


The findings UI: connecting reports to canvas elements

A linter is only as effective as its feedback interface. Clicking any entry in the findings panel highlights the offending elements directly on the canvas:

function renderFindingRow(finding) {
  const row = document.createElement('div');
  row.className = finding-row severity-${finding.severity};
  row.textContent = finding.message;

// Clicking finding highlights offending canvas elements row.onclick = () => { highlightCanvasElements(finding.elementIds); };

return row; }

Rule of thumb: Invest in building a clean graph model once (Union-Find nets), then implement linter rules as pure, crash-isolated functions that link findings directly to canvas DOM elements.
Read this post in the full Engineering Journal →