feat(editor): graph-pulse on errors + store-aware install / add-source

Three connected improvements to the analyzer-driven diagnostics:

  - **Pulsing halo on broken graph nodes**. Every step / pseudo-
    node (inputs / outputs) carrying an analyzer issue now
    breathes a red (error) or amber (warning) halo on the graph
    tab — operator sees the problem on the canvas without
    flipping to text. Honours prefers-reduced-motion: reduce-
    motion users get a static halo at the same intensity. The
    canvas reads severity via a new `stepIssueSeverity` map on
    FlowEditorController; pseudo-nodes use `__inputs__` /
    `__outputs__` sentinel ids.

  - **Store-aware install button**. The analyzer now takes a
    second closure, `storeCapabilities`, listing what the public
    store can install. Unknown-capability issues only carry the
    "Install …" quick-fix when the bare cap is in that list;
    otherwise the issue carries an "Add source for …" fix
    instead. Resolves the asymmetry the operator reported: the
    Store didn't show `htw.digiscout/onet.lookup` but the editor
    happily offered to install it (and would have failed). The
    install path no longer lies about itself.

  - **Did-you-mean suggestion**. When the unknown cap is within
    edit-distance two of an installed or store cap (different
    spelling — distance-0 stays hidden because that's an install
    case, not a typo), the analyzer emits a `ReplaceLineValueFix`
    suggesting the closest match. Preserves the version
    constraint by reusing the installed spec when present
    (e.g. `text.echi@^0.1` → `text.echo@^0.1`).

New public surface:

  - `AddModuleSourceFix` + `AddModuleSourceCallback`
  - `FlowEditorPage.storeCapabilities` + `onAddModuleSource`
  - `FlowAnalyzer.stepSeverity` + the `kInputsNodeId` /
    `kOutputsNodeId` sentinels
  - `FlowIssueSeverity` enum + `FlowNode.issueSeverity`

Tests:
  - Install fix only when in store
  - AddModuleSourceFix as fallback when not in store
  - Did-you-mean replaces install when a near miss exists
  - stepSeverity populated for both step ids and pseudo-nodes

All 36 editor tests green. Bumped to 0.18.0.

Signed-off-by: flemming-it <sf@flemming.it>
This commit is contained in:
flemming-it 2026-06-09 00:21:48 +02:00
parent f43c1ac6cf
commit efdfa7dd79
10 changed files with 497 additions and 45 deletions

View file

@ -85,6 +85,21 @@ class FlowEditorPage extends StatefulWidget {
/// action (the diagnostic remains visible without it).
final InstallCapabilityCallback? onInstallCapability;
/// Host-side "register a new module source" handler. Invoked
/// when the operator clicks "Add source for <capability>…" on
/// an unknown-capability issue that isn't in the public store.
/// Same callback contract as [onInstallCapability]; the host
/// is expected to prompt the operator for a path / URL and
/// then call the Hub install API.
final AddModuleSourceCallback? onAddModuleSource;
/// Capabilities the public store knows how to install. The
/// analyzer uses this to decide whether to show "Install …"
/// (in store) or "Add source for …" (not in store) as the
/// quick-fix on an unknown `use:` line. Empty list = store
/// silent no install button offered.
final List<String> storeCapabilities;
const FlowEditorPage({
super.key,
this.initialFlowName,
@ -93,6 +108,8 @@ class FlowEditorPage extends StatefulWidget {
this.availableCapabilities = const [],
this.style,
this.onInstallCapability,
this.onAddModuleSource,
this.storeCapabilities = const [],
});
@override
@ -117,8 +134,9 @@ class _FlowEditorPageState extends State<FlowEditorPage>
super.initState();
_l = FlowEditorStrings(widget.locale);
_controller = FlowEditorController();
_controller.codeController.setAvailableCapabilities(
() => widget.availableCapabilities,
_controller.codeController.setCapabilityProviders(
available: () => widget.availableCapabilities,
store: () => widget.storeCapabilities,
);
_controller.addListener(_onCtrlChanged);
_controller.codeController.hoverRequest.addListener(_onHoverChanged);
@ -184,7 +202,22 @@ class _FlowEditorPageState extends State<FlowEditorPage>
if (handler == null) return;
final newCaps = await handler(capability);
if (newCaps != null && mounted) {
_controller.codeController.setAvailableCapabilities(() => newCaps);
_controller.codeController.setCapabilityProviders(
available: () => newCaps,
store: () => widget.storeCapabilities,
);
await _controller.codeController.reanalyze();
}
break;
case AddModuleSourceFix(:final capability):
final handler = widget.onAddModuleSource;
if (handler == null) return;
final newCaps = await handler(capability);
if (newCaps != null && mounted) {
_controller.codeController.setCapabilityProviders(
available: () => newCaps,
store: () => widget.storeCapabilities,
);
await _controller.codeController.reanalyze();
}
break;