Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/edda

crates/nornir-saga/.nornir/njord-support-gap.md

nornir-saga ↔ Njord — support gap analysis

Author: Ignalina ApS Date: 2026-07-18 Scope: READ-ONLY on Njord. What must nornir-saga (edda) implement so Njord consumes it as a leaf instead of a vendored copy (reuse-law: kill the twin).

Sides examined:


TL;DR — the headline is: the hard part is already done

  1. API gap = ZERO. Every symbol Njord's saga code consumes exists in nornir-saga 0.1.4 with a matching signature, and the crate compiles clean under Njord's exact feature combos:

    • cargo check -p nornir-saga --no-default-features --features saga → OK (the app/capture build).
    • cargo check -p nornir-saga --features drive → OK (the dev/robot-test build).
  2. The vendored twin is already killed. Njord 466eae6 vendored crates/web/vendor/nornir-saga/; current main (7cba7fe) removed that directory and depends on the real leaf. git ls-tree 7cba7fe | grep -c vendor/nornir-saga → 0.

  3. Njord already consumes the leaf, via a cross-repo path+version dep (Njord/crates/web/Cargo.toml:61 and :106):

nornir-saga = { path = "../../../edda/crates/nornir-saga", version = "0.1.4", default-features = false, optional = true }
# dev-dependencies:
nornir-saga = { path = "../../../edda/crates/nornir-saga", version = "0.1.4", features = ["drive"] }

So there is no blocking work for basic Njord support. What remains is (a) one packaging nit for clean-checkout consumption, and (b) reuse-law debt: generic web capture/replay glue Njord hand-rolls that should live once in the leaf. Ordered below by priority.


Symbol-by-symbol verification (Njord call → leaf definition)

Njord use (file:line) nornir-saga def (file:line) Status
SessionCtx::new(sid,user,app,ver,git_sha) — web/src/saga.rs:26 event.rs:372 (5 args) OK
SagaEvent::nav(route,component,action,state) — web/src/components/saga_capture.rs:50 event.rs:126 (4 args) OK
SagaEvent::click(route,component,action,state) — saga_capture.rs:78 event.rs:137 (4 args) OK
SagaEvent::error(component,detail) — web/src/saga.rs:89 event.rs:157 (2 args) OK
SagaEvent::in_session(&ctx) — web/src/saga.rs:90 event.rs:178 OK
ingest(events,&ctx) -> usize — web/src/saga.rs:45 record.rs:139 OK
MAX_INGEST_BATCH — web/src/saga.rs:40,46 record.rs:21 OK
record(event) — web/src/saga.rs:88 record.rs:110 OK
flush_saga_events(&sink) -> Result<usize> — web/src/saga.rs:65 record.rs:173 OK
JsonFileSink::new(path) — web/src/saga.rs:60, tests/saga_robot.rs:294 sink.rs:50 OK
JsonFileSink::read_all() -> Result<Vec<SagaEvent>> — tests/saga_robot.rs:295 sink.rs:60 OK
ReplayPlan::all_sessions(&events) — tests/saga_robot.rs:297 replay.rs:122 OK
ReplayPlan::is_replayable — tests/saga_robot.rs:298 replay.rs:144 OK
plan.steps — tests/saga_robot.rs:351 replay.rs:79 (pub) OK
step.kind.as_str() / step.action / step.route / step.source_seq — tests/saga_robot.rs:401-406 replay.rs:10,17,12,24 (pub) + StepKind::as_str replay.rs:41 OK
ui::driver::DriveAction — tests/saga_robot.rs:24 ui/driver.rs:35, re-exported ui/mod.rs:58 OK
DriveAction::from_step(step) — tests/saga_robot.rs:352 ui/driver.rs:89 OK
DriveAction::ClickAt { target, .. } — tests/saga_robot.rs:354,452 ui/driver.rs:39 (ClickAt { x, y, target }) OK
DriveAction::Navigate { route } — tests/saga_robot.rs:448 ui/driver.rs:44 OK

Nothing missing, nothing mis-shaped. (Njord's yvra_web::functional_status is Njord's own matrix-emit wrapper, not a saga symbol — see G3.)


Remaining work, by priority

P1 — G1: reusable web-client capture adapter (reuse-law)

Gap. nornir-saga's capture side is egui-only: ui::tap + the emit seam (ui/capture.rs, ui/emit.rs) consume an egui/Elm Msg stream. There is no browser/wasm capture adapter. So Njord hand-writes the entire web client tap in Njord/crates/web/src/components/saga_capture.rs (83 lines):

Every future nornir-saga web consumer rewrites this verbatim. It is generic.

Implement in nornir-saga (new module src/ui/web_capture.rs, behind a new web-capture feature gating web-sys/wasm-bindgen/js-sys):

Njord then deletes saga_capture.rs and calls nornir_saga::ui::web_capture::{emit_nav, install_click_listener}.

P1 — G2: reusable axum server ingest glue (reuse-law)

Gap. Njord/crates/web/src/saga.rs hand-rolls two generic server pieces any axum saga consumer needs:

Implement in nornir-saga (extend the existing web feature, or a new axum-capture feature; the crate already carries axum+tokio behind web, see Cargo.toml):

Njord keeps only its app-specific session_ctx() + the saga_ingest server_fn wrapper (which is genuinely Njord routing) and consumes the two helpers.

P2 — G3: the web replay driver seam is too thin for production

Gap. WebReplayDriver (ui/driver.rs:102-114) is open / dispatch / finish only. It can dispatch a Navigate/ClickAt but cannot:

  1. inject caller-supplied field values at a step (email/password/verification code/orgnr are PII-redacted OUT of a saga by design, so the replay must supply them keyed on the step's target),
  2. query the DOM or assert an outcome (URL landed, element count),
  3. handle per-step pacing (let Leptos settle before the next locate).

Because of this the real Njord robot test bypasses WebDriverDriver entirely: tests/saga_robot.rs imports only ReplayPlan + DriveAction + JsonFileSink and hand-drives fantoccini, doing field injection keyed on target (saga_robot.rs:354-388), pacing (:394-398), and assertions via yvra_web::functional_status (:399-407). The Cargo comment says it outright (Njord/crates/web/Cargo.toml:102-105): "WebDriverDriver has no DOM-query/assert surface". So WebDriverDriver/drive_plan/drive_saga are a demo path, not the one a real consumer uses.

Implement in nornir-saga — make the driver seam production-usable so a consumer stops re-hand-driving the browser. Recommended shape: a ReplayHarness (feature drive) around drive_plan that adds two callback hooks the current trait lacks —

This is the one item that would let Njord's saga_robot.rs run() shrink to a harness call with two closures.

P3 — G4: registry-leaf, drop the cross-repo path dep (Njord-side, noted only)

Njord depends via path = "../../../edda/crates/nornir-saga" (Cargo.toml:61,106), which requires an edda checkout as a sibling directory — fine locally, breaks a clean/CI checkout that doesn't vendor edda next to Njord. nornir-saga 0.1.4 is published on crates.io (verified: default_version 0.1.4), so the leaf is registry-ready. The fix is a Njord-side edit (off-limits here, read-only): drop path =, keep version = "0.1.4", so it resolves from the registry. No nornir-saga work required — recorded for completeness.


Implementation plan (nornir-saga side, ordered)

  1. G1 web-capture feature — src/ui/web_capture.rs: session_id, current_path, emit_nav, install_click_listener(post_fn); gate web-sys/js-sys/wasm-bindgen. Port from Njord saga_capture.rs. Bump to 0.1.5, publish. Njord deletes its copy and consumes.
  2. G2 axum glue — under the existing web feature: spawn_flush(sink, period) + error_layer. Port from Njord saga.rs:57-93. Same publish.
  3. G3 ReplayHarness (feature drive) — inject-hook + verify-hook + pacing around drive_plan, emitting functional_status when testmatrix is on. This is the largest piece; land after G1/G2. Prove it by rewriting the crate's own driver test to the harness (red-when-broken), then Njord adopts.
  4. G4 — Njord-side only (drop path=); do NOT touch Njord here. Note it in the Njord PLAN when the registry consumption is wanted.

Each step is additive (new feature / new fn), so no existing consumer breaks; the lean default build stays dep-free. G1+G2 delete ~120 lines of hand-rolled generic glue from Njord (reuse-law). G3 deletes the bespoke fantoccini drive loop.


Evidence log