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

nordisk/nornir

Cargo.toml

[workspace]
# nornir is the root crate; the test-matrix engine lives in a lean sibling crate
# (EPIC L) so leaf repos can pull just the matrix without nornir's heavy deps.
# `nornir-robotui` (EPIC ROBOTUI) is the reusable robot-UI tester — egui_kittest
# drive + state_json observe + nornir-testmatrix rows — also a lean sibling so
# korp/facett-demo/mannequin can pull it without nornir's heavy deps.
members = [".", "crates/mimir-schema", "crates/mimir-ingest", "crates/mimir-binary", "crates/graphblas-sys", "crates/mimir-graph", "crates/mimir-room", "crates/nornir-selftest", "crates/nornir-build-thing", "crates/nornir-guard", "crates/nornir-jobs", "crates/nornir-introspect", "crates/nornir-viz-web", "crates/bindrift-fixtures", "nornir-demo", "xtask"]
# `crates/mimir-driver` is the NIGHTLY rustc driver (Mímir v2 lane 3): it links
# `rustc_private`, pins its own dated nightly in its `rust-toolchain.toml`, and
# keeps its own Cargo.lock — it must never be built by this stable workspace.
# It is the ONE user of compiler internals in this repo (`tests/no_second_parser.rs`
# pins both halves: excluded here, and nobody else names `rustc_private`).
exclude = ["crates/mimir-driver"]
# The edition-2024 dependency resolver (the 2024 default; explicit so the
# `release gate edition` static check is satisfied and the behaviour is pinned).
resolver = "3"

[package]
name = "nornir"
# 0.5.6: DRIFTED from the published 0.5.5 within the hour. Two real changes landed
# while 0.5.5 was in its 254-second verify-build: the rustup installer lost BOTH
# shells (83b8e50 — `sh -c "curl … | sh"` replaced by a ureq fetch of rustup-init
# plus an argv exec, with an ELF-magic refusal), and a blind guard in
# nornir-introspect was fixed (71593d8). A published crate that still contains
# `curl | sh` is worth a version on its own.
version = "0.6.0"
edition = "2024"
rust-version = "1.85"
description = "Companion to cargo: dependency tracking, release gating, deploy, benchmarks, and documentation assembly. Project-agnostic."
license = "MIT OR Apache-2.0"
repository = "https://git.gunnar.rs/nordisk/nornir"
readme = "README.md"
keywords = ["cargo", "release", "benchmark", "docs", "ci"]
categories = ["development-tools::cargo-plugins", "development-tools::build-utils"]
# Keep the published crate lean: the rendered docs (docs/book.pdf is multi-MB),
# the planning file, and scratch notes are not needed to build or use the crate.
# Anchored with a leading "/" so they don't also match src/docs/.
# Scratch agent/session notes live at the repo root but must NOT ship in the
# published crate (bug #32) — anchor each with a leading "/" so they don't also
# match anything under src/.
exclude = [
    "/docs/",
    "/plan.md",
    "/.nornir/notes/",
    "/new_agents_findings.md",
    "/NEXT_SESSION.md",
    "/wish_list_mcp.md",
    "/znippy_agents_whishes.md",
    "/.cargo/",
    "/.nornir/*.md",
    "/.nornir/*.toml",
    "/.nornir/*.json",
    "/.nornir/.gitignore",
    "/.nornir/assets/*.svg",
    "/.nornir/assets/*.png",
    "/brokkr.toml",
]

[features]
# `server` + `viz` + `mcp` are on by default so a plain `cargo install nornir`
# yields a functional server + CLI client AND the `nornir-ui` viz binary AND the
# MCP surface out of the box — `nornir viz` and the documented
# `claude mcp add … -- nornir-server mcp` both just work with no extra flag (each
# execs/references a binary that is `required-features`-gated, so those features
# MUST be default or you get "No such file" / "Failed to connect").
#
# ⚠ `mcp` is now also in `[[bin]] nornir-server`'s `required-features`, because as
# of 2026-08-03 the MCP stdio server IS the server binary's `mcp` subcommand
# rather than a separate `nornir-mcp`. Dropping `mcp` from default therefore costs
# the daemon too, not just the agent surface. Library/bench
# consumers that want nornir lean (it's a `[dev-dependencies]` for bench harnesses)
# set `default-features = false` — which drops the eframe/egui/wgpu + rmcp stacks.
default = ["server", "viz", "mcp", "net-scan", "docs-export", "robot", "vector", "embed-tract", "embed-ort", "gen-candle", "gen-ollama", "manual-embed", "claude", "embed-holger", "sql"]
# `sql` — the DataFusion SQL surface (`nornir sql`, warehouse::sql::query).
#
# ── IN `default` SINCE 2026-08-24. The reason it was out has EXPIRED. ────────
# It used to say: "OFF by default because datafusion 54 pins arrow ^58.3.0 and
# re-exports its own arrow, so having it in the default graph pins the WHOLE
# constellation to arrow 58." That was true of the arrow-58 era and is not true
# now — the dep is `datafusion = "55"`, and datafusion 55 IS the arrow-59 line
# (see the dep block, which already said so while this comment still didn't).
#
# MEASURED 2026-08-24, not assumed:
#   cargo check --features sql                    -> green, arrow 59.2.0 / datafusion 55.0.0
#   cargo test --lib --features sql warehouse::sql -> 10 passed, 0 failed
# The five that were red — select_where_over_seeded_warehouse, join_across_two_
# warehouse_tables, aggregate_group_by_over_call_edges, call_chain_template_
# renders_and_runs, json_output_is_valid_json_array — were never broken code.
# They unwrap `Err("built WITHOUT the sql feature")` and fail, so a plain
# `cargo test --lib` reported 6 red on a repo that is actually 1058/1058.
#
# PUBLISHABLE, checked against the LIVE registry the same day — both deps this
# feature turns on resolve from crates.io at exactly the versions declared here:
#   datafusion                  55.0.0   == declared "55"
#   iceberg-datafusion-arrow59   0.9.9   == declared 0.9.9
# so a published nornir with `sql` in default resolves without a path or a patch.
#
# The cost is honest and it is the reason this is a decision, not a cleanup:
# every default build now compiles datafusion. Consumers that want nornir lean
# still say `default-features = false`, and `sql` remains a real feature key —
# default-ON, not mandatory. Everything else in warehouse::sql (list_tables,
# describe_table, format_table, format_json, the templates) stays datafusion-free
# and is still available with the feature off.
sql = ["dep:datafusion", "dep:iceberg-datafusion"]

# Convenience bundle: the whole operator surface (server + MCP + docs PDF/HTML + viz).
# `cargo install nornir --features cli`.
cli = ["server", "mcp", "docs-export", "viz", "net-scan"]
# ── ATOM MARKER: `nornir-cli` ────────────────────────────────────────────────
# A PURE MARKER — empty value, no `dep:`, no `cfg` in any module. Its whole job is
# to be a `required-features` key so the `nornir-cli` `[[bin]]` can exist as its
# OWN file next to `nornir` off the same `src/bin/nornir.rs` (see the `[[bin]]`
# block at the bottom of this file and the collision it fixes).
#
# ⚠ RENAMED `thin` → `atom-cli` ON 2026-08-03, with the atom `nornir-thin` →
# `nornir-cli`. A marker feature named `thin` sitting on an atom that measurably
# links the storage engine re-created, one layer down, the exact contradiction
# the atom rename removes. It could not be called `cli` — that name is already
# taken by the convenience bundle four lines above, and a marker silently
# aliasing a real feature bundle is worse than either name.
#
# ⚠ IT IS NOT READ BACK WITH `cfg!`, AND IT CANNOT BE. Until 2026-08-01
# `nornir::atom_line()` named the atom from `cfg!(feature = "thin")`, which looks
# like the obvious non-vacuous proof and is in fact unmeasurable: cargo UNIFIES
# features across every bin of one package in one invocation, so
# `cargo test --features thin` compiles `thin` into `nornir` as well and BOTH
# binaries printed `atom nornir-thin`. `atom_line` now takes
# `env!("CARGO_BIN_NAME")`, which is per-bin and cannot be unified. See
# `.nornir/suite-atoms-design.md` §2.3 for the measured RED.
#
# So this feature's job really is only the `required-features` key — and the real
# artifact difference behind the `nornir-cli` atom is the DECLARED feature set in
# `.nornir/build-variants.toml` (this list minus `embed-holger`), proven by
# building the atom and reading its `--version`.
#
# ⚠ IT NEVER MEANT `linkage = "thin"`, which is why it is no longer spelled that
# way. nornir CANNOT be thin: `skade`, `skade-katalog`, `iceberg` and `parquet`
# are PLAIN (non-optional) deps, so every nornir binary links the warehouse
# engine in-process.
#
# `datafusion` was in that list and did NOT belong there — it is
# `optional = true` (see its dep block). Corrected 2026-08-24. It reaches a
# default build through `sql` being in `default` now, which is default-ON and
# not the same thing as non-optional: `default-features = false` drops it, and
# dropping it does not make nornir thin, because the four above remain.
# `nornir-cli` is `linkage = "mixed"`, and
# `tests/atom_linkage.rs` reads the built binary's symbol table to keep that
# honest rather than asking anyone to believe this comment.
atom-cli = []
# ── ATOM: `nornir-solo` — UI and server in ONE process ───────────────────────
# The `solo` role (nornir_build_thing::plugin::Role::Solo): start it and the GUI
# comes up with `nornir::server_grpc` running INSIDE the same process on a
# background tokio runtime. Nothing else to install, no port to configure, no
# second process to keep alive. Gates the `#[cfg(feature = "solo")]` block at the
# top of `src/bin/urdr-threads.rs` (which is also what makes the `nornir-solo`
# `[[bin]]` a distinct artifact from `nornir-ui`).
#
# `server` + `viz` and nothing else: a solo build that reached a sibling over the
# wire would not be solo, it would be a mixed client.
solo = ["server", "viz"]
# `nornir login` / `whoami` / `logout` — interactive OIDC device-authorization-grant
# (RFC 8628) SSO login, the CLIENT side of holger-style auth. CONSUMES the SHARED
# `nornir-auth-client` (edda) — the ONE client-side login the constellation reuses
# (korp/holger/dwarves), never a device-code twin (L5). The acquired token lives in the
# client's DEFAULT 0600 `~/.nornir/auth-token.json` file store, PREFERRED over the legacy
# static `~/.nornir/token` at the CLI / saga / MCP bearer-resolution sites (the old path
# stays as fallback — a lean or signed-out build behaves exactly as before). `online`
# links the networked device-code request + poll + silent refresh (reqwest via
# `nornir-auth-oidc`); `base64` decodes the JWT subject for `whoami` (display only).
# Opt-in (mirrors `server`/`mcp`): the lean build links none of the auth/reqwest stack.
login = ["dep:nornir-auth-client", "nornir-auth-client/online", "dep:base64"]
# Let the SBOM/security scanner CLONE missing path-dep sibling repos (e.g. nornir's
# `skade`/`skade-katalog`) into the scan root so a headless server-side
# `cargo metadata` resolves on an isolated single-repo checkout. The ONLY scan-time
# network egress (gix clone of the sibling sources). On by default so the live
# `Mimir.SecurityScan` "press Security" path works; DROP it for an airgapped scan
# host: `cargo build --no-default-features --features "server viz mcp"` — the scan
# then degrades to the committed-Cargo.lock fallback with a clear message, never a
# silent reach-out. See `src/security.rs::clone_missing_path_dep_siblings`.
net-scan = []
# 🤖 The Claude viz tab's REAL Messages-API transport: a `reqwest` blocking POST to
# `https://api.anthropic.com/v1/messages`. The HTTP call sits behind the
# `ClaudeTransport` trait seam (src/viz/claude.rs), so the tab + its robot test
# compile and run WITHOUT this feature (a fake transport drives CI; the key-absent
# path is a clear "set ANTHROPIC_API_KEY" state, never a panic). ON by default so a
# stock `nornir viz` can actually talk to Claude; drop it (`--no-default-features`)
# to build lean without reqwest. See `.nornir` / src/viz/claude_tab.rs.
claude = ["nornir-claude/http"]
# Build-tab TEST-BOOT `Vm` mode: drive tunnr's real `tunnr_vm::boot_test` KVM boot
# primitive. OFF by default (the stub runner reports honestly + the build pays
# nothing for the tunnr/QEMU path); a KVM host boots real VMs with
# `cargo build --features test-boot-vm`. See src/viz/test_boot.rs.
# test-boot-vm = ["dep:tunnr-vm"]   # disabled locally: tunnr repo not yet created on Codeberg
# Build-tab TEST-BOOT `Container` mode over the **pure-Rust podman/Docker REST
# API**, routed THROUGH draupnir's ONE OCI engine — the constellation's ZERO-SHELL
# container path: no `std::process::Command("podman")`, no CLI exec, and NO second
# bollard engine (the single engine lives in draupnir). When on, the default
# test-boot container runner is the `DraupnirContainerBootRunner`
# (create+start+logs+kill via draupnir over the API socket), falling back to the
# podman-subprocess `ContainerBootRunner` only when the socket is unavailable. OFF
# by default so a stock build pays nothing for draupnir's bollard/tokio/hyper stack.
# Mirrors knut's `server` feature (`knut-spark-server`). See src/viz/test_boot.rs.
# (Feature name kept for back-compat; it now forwards to draupnir's OCI backend.)
test-boot-container-bollard = ["draupnir/backend-oci"]
# Functional-status MODE passthrough: turn on `nornir-testmatrix`'s `testmatrix`
# feature so components' `functional_status(...)` self-reports are recorded and
# the `Aspect::Functional` runner drains them into matrix rows. OFF by default
# (release strips the emit to a no-op); `nornir test` / CI build with
# `--features testmatrix`. See ../edda/crates/nornir-testmatrix/.nornir/testmatrix-functional-status.md.
testmatrix = ["nornir-testmatrix/testmatrix", "nornir-release-core/testmatrix", "nornir-build-info-flight?/testmatrix"]
# UI-robot tester: fantoccini WebDriver runner for robot/scenarios.toml
# (plan-tests-DAG). `nornir robot run <repo>`.
robot = ["dep:fantoccini"]
# 🤖 MOUNT nornir's viz as a WIRE-REMOTE robot-drive target — the 5th and final
# member of the full-system-autotest fleet. Behind this feature the `nornir-ui`
# binary can bind a Unix socket and serve a `nornir_robotui::cmd_json::CmdJsonServer`
# over the LIVE viz shell (its OWN egui_kittest harness wrapped as a `RobotSession`,
# REUSE — the same drive path `tests/robot_viz.rs` uses), so the fleet drives this
# remote egui process the same way it drives a co-located one. Reached ONLY via
# `nornir-ui --robot-remote <socket>`; a SHIPPED viz never mounts a socket.
# Single-threaded blocking serve (ROOT LAW #0 — spawns nothing). Needs `viz`
# (the shell) + the robotui `wire` channel. See src/viz/robot_remote.rs.
robot-remote = ["viz", "dep:nornir-robotui", "nornir-robotui/wire"]
# 🎬 EGUI saga replay — the egui SIBLING of `robot::replay_plan_to_web_scenario`
# (the WEB path). A recorded session's `nornir_saga::ReplayPlan` converts to the
# SHARED `nornir_robotui::Scenario` and is routed through `run_scenario_mode` so a
# saga replays either HEADLESS (egui_kittest matrix drive) or VISIBLY (the live
# eframe WATCH window) IN the nornir viz — answering "saga can replay on web, now
# it replays on egui too". Reached via `nornir saga egui-replay [--visible]`. Needs
# `viz` (the shell) + the robotui `watch` backend (which pulls eframe + the `wire`
# channel). Off by default so a stock `nornir` never links eframe via robotui.
# See src/viz/saga_replay.rs.
saga-egui = ["viz", "dep:nornir-robotui", "nornir-robotui/watch"]
# 🎬 The egui viz's OWN saga capture adapter: forward `nornir-saga`'s `saga`
# feature so the process-global event buffer compiles in and the viz nav/click
# choke records `SagaEvent`s (drained by the Test tab's ⏺ Record/⏹ Stop toggle
# into the `saga_events` warehouse). OFF by default (release strips every emit to
# an `#[inline]` no-op); `nornir viz`/CI build with `--features saga`. The
# `saga_e2e` proof test is `#![cfg(all(feature = "viz", feature = "saga"))]`.
saga = ["nornir-saga/saga"]
# `nornir saga replay --web`: the axum+SSE UI-movement replay frontend (crate
# nornir-saga's `web` feature). OFF by default — the `nornir saga view/replay`
# terminal + `--drive` mock paths work without it; only the browser page needs
# it. `cargo build --features saga-web`.
saga-web = ["nornir-saga/web"]
# `nornir saga replay --drive <url> --webdriver <endpoint>`: the real-browser
# WebReplayDriver (nornir-saga's `drive` feature, fantoccini). OFF by default;
# without it `--drive` uses the mock driver. `cargo build --features saga-drive`.
saga-drive = ["nornir-saga/drive"]
# Vector / semantic search: a hand-written exact-flat ANN index (cosine over
# f32, runtime-detected AVX-512/AVX2 SIMD + multicore). No external ANN crate,
# no C/FFI — pure Rust. The embedding model (Candle + jina-v2-base-code) layers
# on top under `embed-bundled`.
vector = []
# GPU-batched kNN over the q8 space (`src/vector/gpu.rs`): the same i8 corpus
# `VectorIndex::quantized()` hands the VNNI kernel, scored on a WebGPU compute
# pipeline. OFF by default -- it opens a Vulkan device, which a headless forge
# runner should not pay for on every build.
#
# ADDS NO PACKAGES. Verified against Cargo.lock 2026-08-25: `wgpu 29.0.4` is
# already resolved (eframe/viz pulls it), as are `pollster 0.4.0` and
# `bytemuck 1.25.2`. Turning this on re-uses three crates the default build
# already compiles; it does not widen the graph by one node.
#
# It is deliberately NOT folded into `vector`: `vector` is the pure-Rust,
# no-FFI, airgap-able engine, and a Vulkan loader is a runtime dependency on the
# host's driver stack. Keeping them separate keeps that promise checkable.
vector-gpu = ["vector", "dep:wgpu", "dep:pollster", "dep:bytemuck"]
# `ragnars_emb` IVF index: memory-map the resident int8 blob (the dominant
# allocation) instead of reading it into an owned `Vec`, so the OS page-cache
# backs large corpora and multiple processes share one physical copy.
#
# ⚠ DELETED 2026-08-22, THE SAME BREATH AS THE FLAG IT FORWARDED TO. It read
# `vector-mmap = ["vector", "znippy-zoomies/vann-mmap"]`, and znippy-zoomies
# 97eec95 removed `vann-mmap` because it gated nothing there ("`src/vann/` has
# zero `cfg(feature = "vann-mmap")` sites and does not name `memmap2` at all").
# The forwarding flag was the same shape one layer up: **0** `cfg(feature =
# "vector-mmap")` sites in nornir/src, no manifest in the constellation enabling
# it, and `RagnarsEmbIndex::load_mmap` never existed. Leaving it behind was not
# harmless — a dangling `dep/feature` is a RESOLUTION error, so with the
# znippy-zoomies path dep at its own main, EVERY `cargo build`/`cargo test` of
# nornir failed with "package `nornir` depends on `znippy-zoomies` with feature
# `vann-mmap` but `znippy-zoomies` does not have that feature", before compiling
# one line. If the mmap path is ever written, the flag comes back WITH it.
# 📖 Manual via znippy: pack `docs/book-svg/` into a content-addressed, Arrow-IPC
# `book.znippy` (znippy-compress) and serve pages O(1) into the facett-docview
# lazy viewer (znippy-common `ZnippyReader::extract_file`). Pulled in transitively
# by `manual-embed` (which is in `default`); enable directly with
# `--features viz,manual-znippy` to pack a `book.znippy` without embedding it.
manual-znippy = ["dep:znippy-common", "dep:znippy-compress"]
# Embed the packed `book.znippy` INTO the binary (so it travels with `cargo
# install` — which copies only the binary, never sibling data). On first 📖 Manual
# open it's "plucked out" once to `~/.nornir/manual/book.znippy`, then read O(1)
# from there. build.rs stages an empty placeholder when `docs/book.znippy` is
# absent, so a clean build never breaks (falls back to on-disk manual). In
# `default` so a stock build ships the manual baked in.
manual-embed = ["manual-znippy"]
# EPIC BUILD-THING (task #36) — the real znippy-SEALED build-provenance path.
# Turns on znippy-common's pure-Rust detached-CMS `sign` stack (RustCrypto) +
# znippy-compress (archive carriage), so `src/build/seal.rs` can CMS-sign a
# build-provenance attestation digest and pack it into a content-addressed
# `.znippy`. OFF by default: a stock build emits an honest UNSEALED attestation
# (record + digest, no faked signature). `manual-znippy` guarantees
# znippy-common + znippy-compress are present even in a --no-default-features
# build.
build-provenance = ["manual-znippy", "znippy-common/sign"]
# HEAVY container-image build path: forward `nornir-build-thing/image-build` so the
# real `BollardImageBuilder` (podman/Docker REST API, zero-shell) is linkable from
# `nornir::build` and the `nornir build --image <name>` CLI path can actually build
# an OCI image. OFF by default (and NOT in `cli`/`default`) so the dep-light default
# stays free of the bollard/hyper/tokio stack — mirrors `test-boot-container-bollard`.
# The dep-light VM provenance path (`PrebuiltVmBuilder`) needs none of this.
build-image = ["nornir-build-thing/image-build"]
# Columnar build-info Arrow binding: forward `nornir-build-thing/build-info-arrow`
# so `nornir-build-thing::build_info::{build_info_schema, build_info_batch}` link
# from the root crate. OFF by default (the warehouse `build_info` landing path
# builds its own `arrow` RecordBatch, so it does NOT need this) — this feature is
# for callers that want build-thing's own §f3 Arrow emitter. A root forward for
# `cargo build --features build-info-arrow` (the crate error the slice-2 wave hit
# when a sibling feature dangled). Guarded by `tests/cargo_features_guard.rs` so a
# feature can never again reference an undeclared `dep:`.
build-info-arrow = ["nornir-build-thing/build-info-arrow"]
# Slice 5 — the Flight-backed `BuildInfoSink`. Ships build-info rows to a remote
# `edda/nornir-flight` endpoint via `do_put` (and a `do_get`/`do_exchange`
# consumer helper). The engine now lives in the `nornir-build-info-flight` edda
# leaf (the one crate that deps BOTH `nornir-build-thing` and `nornir-flight` —
# no cycle); nornir keeps a re-export shim at `build::flight_sink` (shrink plan
# Tier-1). OFF by default: the in-process `InMemBuildInfoSink` stays the default
# (zero transport tax, no second arrow). Turn on with
# `cargo build --features build-info-flight`.
build-info-flight = ["dep:nornir-build-info-flight"]
# Canonical Maven Central JAR fixtures (guava, ecj, scala, kotlin) for
# decompression / zip benchmarks. Opt in so the core library carries
# no project-specific data.
fixtures-maven = ["nornir-bench/fixtures-maven"]
# ── wire-client — the CLI→server tonic client stack, declared ONCE ───────────
# `NORNIR_SERVER`/`--server` routes a verb to a running `nornir-server` instead of
# the embedded in-process path. Every one of those remote wrappers in
# `src/bin/nornir.rs` needs the SAME four deps: the generated `nornir.v1` stubs
# (`nornir-wire`), the service descriptors (`nornir-service`), and tonic + prost.
#
# ⚠ WHY THIS FEATURE EXISTS (SK290, measured 2026-08-01). Those ~50 wrappers were
# gated `#[cfg(any(feature = "mcp", feature = "server"))]` — a HAND-MAINTAINED
# PROXY for "the wire stack is present". `viz` brings the identical four deps, so
# the proxy was wrong for it: `server_target()` is ungated, so the leanest declared
# atom (`nornir-ui`, `--no-default-features --features viz`,
# `.nornir/build-variants.toml` ATOM 4/7) called `client_workspace()`,
# `run_bench_remote()`, `run_test_matrix_remote()`, `submit_bakeoff_remote()` and
# `select_bakeoff_rows()` unconditionally and could not link:
#
#   error[E0425]: cannot find function `client_workspace` in this scope
#     --> src/bin/nornir.rs:9689:21
#   note: found an item that was configured out
#
# Naming the condition makes it exact instead of approximate: `mcp`, `server` and
# `viz` each list `wire-client`, the gates say `#[cfg(feature = "wire-client")]`,
# and a fourth feature that pulls the wire stack in cannot forget to update 50
# cfg attributes. It is a pure aggregate — enabling it alone is a valid build.
wire-client = ["dep:tonic", "dep:prost", "dep:tonic-prost", "dep:nornir-service", "dep:nornir-wire"]
# Compile the MCP surface IN: exposes every nornir API as an MCP
# (Model Context Protocol) tool over stdio for LLM agents, served by
# `nornir-server mcp` (`src/mcp_stdio.rs`). Was the separate `nornir-mcp` binary
# until 2026-08-03 — see the `[[bin]] nornir-server` block. NOTE: today each
# tool opens the iceberg warehouse in-process (direct `IcebergWarehouse::open`),
# NOT via gRPC to a running `nornir-server` — the tonic-client conversion is
# still pending (see plan.md §"Server / transport").
mcp = ["dep:rmcp", "dep:schemars", "dep:tracing-subscriber", "wire-client"]
# Build the `nornir-server` binary: a tonic gRPC server that mirrors
# every CLI/MCP tool one-for-one. Bearer-token auth via interceptor.
# mTLS (with CN=<username>) is a planned future addition (see README).
server = ["wire-client", "dep:tokio-stream", "dep:tracing-subscriber", "dep:nornir-auth-policy", "code-room", "mimir-container"]
# ── THE CODE ROOM ENGINE (Mímir v2) ─────────────────────────────────────────
# Links `mimir-ingest` (the consumer: git history → the skade event log) and
# `nornir-mimir-room` (the read side: the log folded into SuiteSparse:GraphBLAS
# matrices). GraphBLAS is a BUILD REQUIREMENT of everything that serves a code
# room (`libgraphblas-dev` 7.x to build, `libgraphblas7` to run) — this feature
# is NOT a way to run a server without it. Its one job is LINKAGE: `server`
# turns it on (so `full-server`, `solo` and the default CLI carry the engine),
# and the thin atoms (`nornir-ui`, wasm) that only ask a server over gRPC do
# not link it. With it off, `nornir::code_room` still compiles and every
# surface answers `not_linked`.
code-room = ["dep:mimir-ingest", "dep:nornir-mimir-room", "dep:nornir-mimir-graph"]
# The Mímir consumer's CONTAINER ROAD (code_room::container): every driver /
# cargo run in the pinned toolchain image, booted per call through draupnir's
# ONE OCI engine (NORNIR_MIMIR_CONTAINER_IMAGE). On in `server`.
mimir-container = ["code-room", "draupnir/backend-oci"]
# The HEADLESS server shape the fat `dwarves-full` node embeds: nornir's full gRPC
# stack (`server`) with NO viz — no eframe, no wgpu — so the forge can compile this
# lib IN and stand `:7878` up on its own runtime beside `:7900`.
#
# It used to also turn on `dwarves/server` + `dwarves/grpc-web` + `dwarves/warehouse`
# and gate a `dwarves-full` BIN here, because nornir path-dep'd dwarves and a
# dwarves-side bin pulling the `nornir` umbrella would have been a cargo cycle. That
# edge is gone: nornir no longer names dwarves at all, so the fat bin moved to the
# forge (`dwarves`, behind its `embed-nornir` feature) where it always belonged —
# the forge is DOWNSTREAM of nornir and is the member allowed to embed siblings.
full-server = ["server"]
# Marry nornir to holger: embed `holger-server-lib` IN-PROCESS so `nornir cache
# up`, `release stage --execute`, and (optionally) the nornir-server daemon run
# the /cache crates.io-mirror + writable /sparring registry as tracked jobs —
# no `holger-server dev-pair` subprocess. NOW IN `default` (N6): a stock
# `nornir viz` ships the embedded /cache + /sparring dev pair so the 📦 Holger
# tab surfaces the live registry instead of the "holger is not embedded" note.
# Every use site still has a `#[cfg(not(feature = "embed-holger"))]` fallback, so
# a `--no-default-features` client stays lean (and a crates.io publish that must
# not drag in the private holger artifact server drops it there). Pulls holger's
# tonic 0.13 + rustls stack (unified with nornir's own gRPC, also 0.13).
embed-holger = ["dep:holger-server-lib"]
# Build the documentation exporter: parses markdown and renders to PDF/HTML
# via the typst engine (100% Rust, no shellout, no JS). Bundles a default
# typst template + embedded fonts via typst-kit.
#
# The FEATURE NAME stays (korp/dwarves and this crate's own cfgs key on it) but
# the typst stack left the building: it now rides `nornir-docgen/export` (the
# svg charts, book, apidoc, ai_context) and `nornir-docbook` (the render engine,
# still re-exported as `docs::export` and consumed by `docs::release_book`).
# nornir's own direct typst-*/pulldown-cmark/rust-embed deps are gone with it.
docs-export = ["nornir-docgen/export", "dep:nornir-docbook"]
# Build the `nornir-ui` binary (the Urðr Threads product; the bin was renamed
# from `urdr-threads` on 2026-08-01): an egui/eframe time-travel
# visualizer for the Urðr warehouse — paints one swim-lane per repo,
# git SHAs as nodes along the release timeline, and cross-repo
# dep-graph edges as woven threads between lanes. Pure Rust
# (wgpu/glow back-end); no JS, no C deps beyond the platform's own
# windowing libs that eframe pulls in.
viz = ["wire-client", "dep:eframe", "dep:egui", "dep:image", "dep:egui-snarl", "dep:facett-jobview", "dep:facett-docview", "dep:facett-graphview", "dep:facett-graph3d", "dep:facett-plandag", "dep:facett-warehousedeck", "dep:facett-helix", "dep:facett-core", "dep:facett-brokkr", "dep:facett-app", "dep:facett-graphpan", "dep:facett-matrix"]
# Semantic-search embedders. The `vector::Embedder` trait is the interface;
# two interchangeable backends run the SELECTED model's ONNX export (the model
# is chosen from `src/vector/embed_registry.rs` via $NORNIR_EMBED_MODEL —
# default jina-embeddings-v2-base-code, 768-dim, QK-LayerNorm "code" arch). The
# build.rs fetch + the runtime load read the same selection so they never
# disagree. Pick one backend (or both):
#
#   embed-tract — CPU, `tract-onnx`. Pure-Rust runtime. The always-present
#                 fallback the runtime selector lands on when no GPU is usable.
#   embed-ort   — GPU/CPU, `ort` (ONNX Runtime), **dynamic-load**. Requests the
#                 CUDA (or ROCm) execution provider; the runtime selector
#                 (`src/vector/select.rs`) only routes to it when onnxruntime +
#                 a GPU are actually present, else it picks tract.
#
# #9 (runtime GPU/CPU/AMD selection — both backends DEFAULT): both `embed-tract`
# AND `embed-ort` are in `default`, so ONE `cargo install nornir` binary carries
# the CPU and GPU embedders and picks at RUNTIME (see `vector::load_embedder` →
# `vector::select`). The build stays clean on a GPU-less / onnxruntime-less box
# because `embed-ort` uses ort's `load-dynamic` (→ `ort-sys/disable-linking`):
# the binary LINKS NOTHING from onnxruntime; it `dlopen`s `libonnxruntime.so` at
# runtime ONLY if a GPU path is selected. Absent libonnxruntime.so → tract CPU.
#
# ⚠ EXCEPTION to the no-C rule (accepted by the owner, 2026-06-04): both backends
# need `tokenizers` (onig C + esaxx C++). `embed-ort` does NOT link the
# onnxruntime C++ lib anymore (load-dynamic) — it loads it at runtime if a GPU is
# used. tract is pure Rust. Model weights (onnx + tokenizer.json) are fetched
# once by build.rs into a local cache.
#
# HONEST build-prerequisite: a default build needs NO onnxruntime to compile or
# to run on CPU (tract). The GPU path additionally needs `libonnxruntime.so`
# resolvable at runtime (via $ORT_DYLIB_PATH, /opt/nornir/cuda, or a system
# dir) plus the CUDA/ROCm runtime libs. When any of that is missing the selector
# silently uses tract CPU — never a build failure, never a panic.
embed-tract = ["vector", "dep:tract-onnx", "dep:tokenizers"]
embed-ort = ["vector", "dep:ort", "ort/load-dynamic", "dep:tokenizers", "dep:libloading"]
# embed-ort-rocm (G1) — AMD ROCm/MIGraphX execution-provider path for the ort
# embedder. Builds on embed-ort and turns on ort's `migraphx` + `rocm` EP
# features (which link the ROCm provider shims). The EP is only *requested* when
# the runtime probe (src/vector/rocm.rs::available) finds ROCm installed, so a
# build with this feature on a non-AMD box degrades cleanly to CUDA-then-CPU.
# OFF by default → non-AMD builds are completely unaffected.
embed-ort-rocm = ["embed-ort", "ort/migraphx", "ort/rocm"]

# ── Generative-LLM backends (EPIC #39) ──────────────────────────────────────
# The `Generator` trait (src/warehouse/generator/) is ONE interface with THREE
# real, feature-gated implementations behind the `generator(spec)` factory. All
# three are OFF by default so a plain `cargo build` stays light — the factory's
# arm for an absent backend returns a clear "not compiled" error, and
# `available()` runtime-probes so a missing model/lib degrades gracefully
# (never a canned-text stub). Selectable via `--gen <spec>` / `$NORNIR_GEN_BACKEND`.
#
#   gen-candle    — pure-Rust generation via candle + candle-transformers
#                   (quantized GGUF qwen2/llama family). No C deps.
#   gen-mistralrs — the `mistralrs` crate's in-process API (candle-based).
#   gen-onnx      — generative via the ORT (onnxruntime) stack + tokenizers.
#                   Links onnxruntime (C++) — same accepted exception as embed-ort.
#
# `gen-ollama` is NOT one of the three backends: it's the prior-art ollama HTTP
# client, kept OFF by default as a convenience (`--gen ollama:<tag>`). It pulls
# no new dependency — it reuses the already-vendored `ureq`.
gen-candle = ["dep:candle-core", "dep:candle-transformers", "dep:tokenizers", "dep:hf-hub"]
gen-mistralrs = ["dep:mistralrs"]
gen-onnx = ["dep:ort", "dep:tokenizers"]
gen-ollama = []

# ── EPIC AIRGAP — Skíðblaðnir ────────────────────────────────────────────────
# Turn on the znippy airgap archive engine (content cache + delta + O(1)
# selective read) inside skidbladnir. With this on, `nornir install upgrade
# --delta` ships only the changed blobs (the airgap-kubernetes speed story) and
# `product::delta_supported()` reports true. OFF by default keeps the lean
# tar+zstd fallback that always works offline.
airgap-znippy = ["skidbladnir/znippy"]

[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
# Process-group SIGKILL for the Build-tab TEST-BOOT lifecycle (Exe/Container boot
# jobs spawn a child in its own process group; killing the job SIGKILLs the whole
# group). std has no `killpg`, so we reach for libc's `kill(-pgid, SIGKILL)`.
libc = "0.2"
# The tunnr VM boot primitive (`tunnr_vm::boot_test(BootSpec) -> BootHandle`) that
# the Build-tab TEST-BOOT `Vm` mode drives. Optional (feature `test-boot-vm`) — the
# default build keeps the honest stub runner and pays nothing for QEMU/OVMF; a host
# with KVM boots real VMs with `--features test-boot-vm`. Sibling path dep (offline
# workspace); tunnr is read-only from nornir's side.
# tunnr-vm = { path = "../tunnr", optional = true }   # disabled locally: tunnr repo not yet created on Codeberg
# draupnir — the shared boot/provisioning library that owns the ONE bollard/podman
# REST engine for the constellation. The Build-tab TEST-BOOT `Container` mode routes
# THROUGH it (no second engine here). Lean by default (draupnir's default build is
# pure-std, zero deps), so the delegation seam + its mock tests compile in a stock
# build; the LIVE OCI engine is pulled only by `test-boot-container-bollard =
# ["draupnir/backend-oci"]`. Sibling path dep (offline workspace).
draupnir = { path = "../draupnir", version = "0.2.0", default-features = false }
# The pure workflow-DAG core, extracted from src/funnel/ (#22). nornir's
# src/funnel/mod.rs re-exports it so all `crate::funnel::*` paths still resolve.
# Floor 0.1 -> 0.1.4: `Event::PlanComponentWired` exists in 0.1.4 and in no earlier
# published copy — E0599 x3 in a Gate B verify build. funnel-core 0.1.3 had DRIFTED:
# same version, different bytes, the variant only in the local tree.
#
# ⚠ NO `path` — AND THAT IS THE FLOOR'S DOING, NOT AN OVERSIGHT. A `version` on a
# path dep constrains THE SIBLING ON DISK as well as the registry, and
# `../funnel-core`'s own `[package] version` is still `0.1.3` (its repo never
# committed the bump that was published as 0.1.4). So `{ path = "../funnel-core",
# version = "0.1.4" }` — main at 0d1e965 — could not RESOLVE:
#
#   error: failed to select a version for the requirement `funnel-core = "^0.1.4"`
#   candidate versions found which didn't match: 0.1.3
#   location searched: ~/git/funnel-core
#
# Resolution happens before compilation, so that one line took down every target,
# every test and every CI step in the workspace at once. Keeping the floor and
# dropping the path is the only shape that is true on both sides: 0.1.4 is what
# nornir needs, and crates.io is the only place a 0.1.4 exists. RESTORE THE PATH
# the moment funnel-core's repo declares >= 0.1.4 — `tests/cargo_features_guard.rs
# ::every_path_dep_version_floor_is_satisfiable_by_its_sibling_checkout` now fails
# RED, naming both numbers, if any path dep is put back into this state.
funnel-core = { version = "0.1.4" }
# EPIC L — the reusable multi-aspect test-matrix engine (runner + pure model +
# Aspect engine + TestSink). nornir re-exports it so existing paths
# (`nornir::test_matrix::*`, `nornir::warehouse::test_results::{TestResultRow,…}`)
# keep working, and implements `TestSink` over the Iceberg warehouse.
nornir-testmatrix = { version = "0.2.10", path = "../edda/crates/nornir-testmatrix" }
nornir-release = { path = "../edda/crates/nornir-release" , version = "0.1.4" }
# nornir-humanize — the ONE shared human-readable formatter (edda, LAW #5). nornir's
# `bin/nornir.rs::human_bytes` was a byte-for-byte twin of its IEC `bytes()`; consumed
# here instead of re-hand-rolled. std-only, zero runtime deps.
nornir-humanize = { version = "0.1.3", path = "../edda/crates/nornir-humanize" }
# nornir-claude (package `ln`) — the ONE shared Anthropic Claude Messages-API client
# (edda, LAW #5). nornir's `viz/claude.rs` was a byte-parallel transport/wire/error/
# consts twin; consumed here instead. facett-claude + dwarves are the other consumers.
# The live `reqwest` POST rides the `http` feature (nornir's `claude` feature forwards
# to it); nornir keeps ONLY its domain file-key ladder (<home>/.nornir/anthropic-key)
# and the funnel/tab wiring on top of `ln::send_request`.
nornir-claude = { version = "0.1.1", path = "../edda/crates/nornir-claude" }
# autonom — the shared completeness / wiring-coverage subsystem (edda). nornir's
# `src/autonom/` is a THIN consumer: it delegates the allowlist file model + the
# sentinel-ban scan here, and re-exports the gate façade. Renamed dep key
# (`edda_autonom`) so it doesn't clash with nornir's own `crate::autonom` module.
# See ../edda/crates/autonom/.nornir/autonom-consolidation.md.
edda_autonom = { package = "autonom", version = "0.2.0", path = "../edda/crates/autonom" }
# EPIC AIRGAP — Skíðblaðnir: the 100% Rust airgap pack/move/unfold/install
# framework. nornir's `airgap` CLI verb drives it; the crate stays lean (its own
# minimal deps) so leaf consumers (holger, tunnr) can adopt it without nornir's
# stack. Extracted out of nornir/crates into its own sibling repo (Skidbladnir)
# so it can be released without a nornir release-cascade; nornir consumes it from
# the sibling checkout. Package name stays `skidbladnir` (published under it).
skidbladnir = { version = "0.2.0", path = "../Skidbladnir" }
# Saga — record/replay engine (task #25): capture user navigation → nornir
# warehouse `saga_events` → the matrix replays the path as a robot test. nornir
# implements `SagaSink` over its Iceberg warehouse and reads sessions back into
# the 🧪 Test tab's Sessions pane. Pulled WITHOUT the `saga` capture feature —
# the warehouse sink + replay planner + viz read use only the serde types; the
# `record()`/`drain()` capture buffer (a process-global) is the consuming app's
# opt-in (facett/Njord, phase 2), so release pays zero cost here.
# Moved into edda (the nordisk shared-lib samling) — zero consumers outside
# nornir at move time, so it releases from there now; nornir path-deps it back
# from the sibling checkout (mirrors the `skidbladnir` wiring above).
nornir-saga = { version = "0.2.0", path = "../edda/crates/nornir-saga" }
nornir-git = { version = "0.2.0", path = "../edda/crates/nornir-git" }
# The multi-repo `nornir-workspace.toml` descriptor (parse + repo-source resolve),
# lifted into edda so dwarves can consume it FAT without an edda->nornir cycle
# (warehouse-free leaf). Re-exported verbatim as `nornir::workspace`.
nornir-workspace = { version = "0.1.3", path = "../edda/crates/nornir-workspace" }
# The shared cross-repo dependency-graph core (compute + query + the pure,
# egui-free dependency-VIEW layout), lifted out of nornir so nornir AND dwarves
# render the same suite dependency view. nornir keeps only the `cargo_metadata`
# builder + the warehouse persist (`warehouse::dep_graph`); the rest re-exports.
nornir-depgraph = { version = "0.1.3", path = "../edda/crates/nornir-depgraph" }
# The FEATURE ACTIVATION graph parsed out of `Cargo.toml` — the producer behind
# the `feature_dep_edges` warehouse table. A true leaf (toml + serde + the
# zero-dep default build of `nornir-graph`), because `cargo metadata` provably
# CANNOT answer the question: it has no `-p`, always resolves the whole workspace
# including dev-deps, and flattens the weak `pkg?/feat` AND-groups away. nornir
# owns only the Iceberg persist (`warehouse::feature_dep_edges`) and the populate
# hook (`monitor` step 7d); the parse + the row TYPE live in the leaf, so the
# table's writer and its in-memory consumers cannot drift apart (L5).
nornir-feature-graph = { version = "0.2.0", path = "../edda/crates/nornir-feature-graph" }
# The pure, warehouse-free doc RENDER engine (markdown → typst → PDF/HTML/SVG),
# lifted verbatim out of `docs/export.rs`. nornir keeps `docs::export` as a
# byte-stable re-export shim over this leaf; the assembly/history half
# (sections/warehouse/release_book/search) stays here (it reads the warehouse
# and is genuinely Mímir). Optional — only the `docs-export` feature pulls it.
nornir-docbook = { version = "0.1.6", path = "../edda/crates/nornir-docbook", optional = true }
# The warehouse-free doc-book + mashup ASSEMBLY half (the nornir:gen marker
# engine + every section renderer, layout, book, bench_status, test_inventory,
# hooks, pages, svg, apidoc, ai_context), lifted verbatim out of `src/docs/`.
# `docs::*` re-exports it byte-stable; the halves that read the Iceberg
# warehouse (docs::warehouse, docs::search, docs::release_book) stay here. NOT
# optional: the marker engine (sections/layout/bench_status/…) has always been
# in every build — only the typst-backed `export` half is feature-lit (by
# `docs-export` above). The `depgraph` section's network half is injected via
# `Ctx::path_dep_cloner` (see `security::docs_path_dep_cloner`).
# NOTE (release cascade): `nornir-docgen` is `publish = false` until
# nornir-docbook publishes — same standing follow-up the docbook shim opened.
# FLOOR 0.1.1 (2026-08-27): 0.1.1 is where the `nornir:claim:` gate moved INTO the
# engine, off this crate's six wrappers. `^0.1.0` would let a registry build
# resolve the ungated 0.1.0 and get a render that cannot refuse a false claim —
# the exact "engine shipped, door forgotten" shape the gate closes.
nornir-docgen = { version = "0.2.0", path = "../edda/crates/nornir-docgen" }
nornir-cli-outcome = { version = "0.1.4", path = "../edda/crates/nornir-cli-outcome" }
# The ONE shared content-hash primitive (edda leaf) — sha256 + lowercase-hex,
# consumed instead of re-rolling `Sha256::new()` + a private hex loop in every
# content-addressed store (warehouse blob_store, docs/arch/vector/index/introspect
# persist). Dep-light (sha2 only, hex hand-rolled). See ../edda/crates/nornir-hash/.
nornir-hash = { version = "0.1.1", path = "../edda/crates/nornir-hash" }
# The shared tonic service scaffold (edda) — the ONE bearer-auth interceptor +
# constant-time token compare the `nornir-server` binary consumes instead of
# hand-rolling its own (Law #1). Optional; lit by the `server` feature alongside
# `tonic`/`prost`. See ../edda/crates/nornir-service/.
# Floor raised 0.1.0 → 0.1.8 (2026-08-23, release cascade). `dwarves release_gates`
# blocked nornir 0.5.5 on UnderstatedFloor: every published nornir-service the old
# caret admitted lacks the `client` module and the `pub use tonic` this crate's
# wire-client speaks, and the `path =` beside the requirement hid that — cargo
# resolves the path locally, so neither the workspace build nor the rehearsal
# could see it. 0.1.8 (edda 11610e6) is the first published version that carries
# both.
nornir-service = { version = "0.1.12", path = "../edda/crates/nornir-service", optional = true }
# The wire/transport leaf (edda) — owns `proto/nornir.proto` + its generated
# `nornir.v1` prost/tonic client+server stubs (`nornir_wire::pb`). The proto
# codegen that used to live in this crate's `build.rs` moved there so the
# producer (server) and the read-clients share ONE generated `pb` instead of
# each `include_proto!`-ing a private copy. Optional; lit by `server`/`mcp`/`viz`
# (the tonic-wire gate). See ../edda/crates/nornir-wire/.
# Floor 0.1.0 -> 0.1.1: `MimirSvcTrait` is the proto-generated `pb::mimir_server::Mimir`,
# and published 0.1.0 predates `semantic_clones` plus the current `record_events` /
# `list_sessions` signatures — E0407/E0053 in the verify build.
nornir-wire = { version = "0.1.3", path = "../edda/crates/nornir-wire", optional = true }
# The async auth POLICY layer (edda) — `PolicyAuthLayer` (server-wide tower Layer)
# + `AuthConfig::load_or_static`. Replaces the per-service bearer interceptor with
# one server-wide layer: empty ⇒ open (behaviour-preserving), a non-empty token ⇒
# bearer required, and `NORNIR_AUTH_CONFIG` switches on OIDC/OAuth2/mTLS/RBAC. Lit
# by `server`. `auth-bearer` pins the static-token provider the default path needs.
nornir-auth-policy = { version = "0.1.4", path = "../edda/crates/nornir-auth-policy", optional = true, features = ["auth-bearer"] }
# The Flight-backed `BuildInfoSink` (edda leaf) — ships build-info rows to a
# remote `nornir-flight` endpoint over `do_put` (and a `do_get` consumer helper),
# the one crate that deps BOTH `nornir-build-thing` and `nornir-flight`. Lit by
# `build-info-flight`; nornir keeps a re-export shim at `build::flight_sink`
# (shrink plan Tier-1). See ../edda/crates/nornir-build-info-flight/.
nornir-build-info-flight = { version = "0.2.0", path = "../edda/crates/nornir-build-info-flight", optional = true }
# The SHARED client-side login leaf (edda): the RFC 8628 device-code grant + silent
# refresh + `TokenStore`/`TokenBundle`, consumed identically by korp/holger/dwarves —
# `nornir login` is the fourth consumer, never a device-code twin (L5). Lit by the
# `login` feature (with `nornir-auth-client/online` for the networked grant); the lean
# build links none of the auth/reqwest stack. `default-features = false` keeps the offline
# `cached_token` surface available without pulling the online HTTP client unless asked.
nornir-auth-client = { version = "0.1.1", path = "../edda/crates/nornir-auth-client", optional = true, default-features = false }
# JWT payload decode for `nornir whoami` (display-only subject; no signature check —
# that is the server's job on presentation). Optional; lit by `login`.
base64 = { version = "0.23", optional = true }
nornir-selftest = { version = "0.1.1", path = "crates/nornir-selftest" }
nornir-guard = { version = "0.1.1", path = "crates/nornir-guard" }
nornir-jobs = { version = "0.2.0", path = "crates/nornir-jobs" }
# Mímir v2 contract: code_nodes/code_edges Arrow schemas + THE SCIP moniker
# formatter. coderoom's `scip_moniker` calls it (one writer, LAW 5); the nightly
# mimir-driver depends on the same crate by path.
mimir-schema = { version = "0.1.0", path = "crates/mimir-schema" }
# The code room engine, behind `code-room` (see the feature): the consumer and
# the read side. `nornir-mimir-room` links the system GraphBLAS.
mimir-ingest = { version = "0.1.0", path = "crates/mimir-ingest", optional = true }
nornir-mimir-room = { version = "0.1.0", path = "crates/mimir-room", optional = true }
# the clone signals' FastRP propagation (`clone_signals::role`): one weighted
# mxv per projected column, on the room's own GraphBLAS engine
nornir-mimir-graph = { version = "0.1.0", path = "crates/mimir-graph", optional = true }
# 0.1.5 floor: the `CoverageRow` POD moved into the leaf (warehouse::coverage
# re-exports it; the Iceberg I/O stays here).
nornir-coverage = { version = "0.1.5", path = "../edda/crates/nornir-coverage" }
nornir-trace = { version = "0.1.0", path = "../edda/crates/nornir-trace" }
nornir-introspect = { version = "0.1.4", path = "crates/nornir-introspect" }
# `runner` — the OUTER bench runner (find the repo's `nornir-bench` example,
# build it, run it, parse the `BenchRun`). It used to be ~300 lines inside
# `release::pipeline`; the leaf owns it now, and `release::pipeline` re-exports
# it so every call site is unchanged (LAW 5). NOT optional here: the release
# gate's bench leg and `Ops.RunBench` both go through it in every build.
nornir-bench = { version = "0.2.0", path = "../edda/crates/nornir-bench", features = ["runner"] }
# The nornir.toml loader + the $HOME-derived data-root rule, lifted into an edda
# leaf (migration-ledger §N prereq) so dwarves can consume it FAT without a
# nornir->dwarves cycle. nornir re-exports it verbatim as `nornir::config` (see
# lib.rs), so every `crate::config::…` / `nornir::config::…` call site resolves
# unchanged. The `[build]` schema + `BenchSpec` methods it owns pull the
# build-thing + bench leaves it already depended on.
nornir-config = { version = "0.2.0", path = "../edda/crates/nornir-config" }
# The backend-neutral Warehouse SEAM (trait + POD arg/return types + the pure
# knowledge model structs its async surface names), extracted to edda so a
# consumer (dwarves, …) can depend on the trait WITHOUT depending on nornir.
# nornir re-exports every type at its old module path (`crate::warehouse::…`,
# `crate::knowledge::…`) so this extraction is invisible to existing call sites.
# Floor 0.1.0 -> 0.1.4: the published 0.1.3 and below are on ARROW 58, so their
# `WarehouseBackend` signatures take `arrow_array::RecordBatch` from a different
# arrow major than the `arrow::array::RecordBatch` this crate passes — E0053 on
# all eight methods (`append_arrow`, `scan_arrow`, `scan_filtered`, `scan_limited`,
# `ensure_table`, `ensure_columns`, and the two `_async` twins). 0.1.4 is the first
# published version on arrow 59. Third floor Gate B caught on this crate today.
nornir-warehouse-trait = { version = "0.2.0", path = "../edda/crates/nornir-warehouse-trait" }
# The three RELEASE warehouse tables — `release_lineage` / `release_events` /
# `release_changes` — their canonical Iceberg schemas AND their Arrow codecs, as
# ONE definition shared with the release orchestrator (L5). The orchestrator
# writes these very tables into this very warehouse but may not take a crate edge
# on this umbrella, so for a while both halves carried their own hand-maintained
# copy — and a column added to one and not the other would have produced mutually
# incompatible tables with nothing failing at compile time. `warehouse::{
# iceberg_schema, release_events, release_changes}` now re-export the leaf, so
# every existing `crate::warehouse::…` call site is untouched.
nornir-release-ledger = { version = "0.2.0", path = "../edda/crates/nornir-release-ledger" }
# The ONE reader over the persisted knowledge map (`symbol_facts` / `call_edges`):
# `KnowledgeView`, the memoized inverted index behind `callers_of`/`callees_of`/
# `call_path`, and the two-phase snapshot-pushdown loader. `knowledge::query` used
# to define these AND the release orchestrator's SEMANTIC-BLAST gate kept a
# hand-maintained copy of them (`load_latest_audited` + `call_path` were the two
# largest duplicated function bodies in the constellation). `stree` keeps the R2.1
# `stree32` batched accelerator that moved with the reader, so `callers_of_batch` /
# `callees_of_batch` and their bench arm are unchanged.
nornir-knowledge-view = { version = "0.2.0", path = "../edda/crates/nornir-knowledge-view", features = ["stree"] }
# EPIC BUILD-THING (task #36) — the reusable, dependency-light build-variants +
# build-provenance core, extracted as a leaf crate (mirrors nornir-testmatrix) so
# a leaf repo's xtask can build a declared variant + capture provenance without
# nornir's heavy stack. Re-exported from `src/build/mod.rs`.
nornir-build-thing = { version = "0.2.0", path = "../edda/crates/nornir-build-thing" }
# The compile-and-test codegen JUDGE (edda leaf, shrink plan Tier-1): the pure
# `AnswerJudge`/`CodegenJudge`/`Verdict` scorer shared by the bake-off and the
# funnel accept/reject oracle. nornir re-exports it at `warehouse::codegen_judge`.
nornir-codegen-judge = { version = "0.1.0", path = "../edda/crates/nornir-codegen-judge" }
# The pure chrono poll-cadence gate (edda leaf, shrink plan Tier-1):
# `parse_interval`/`is_workspace_due`/`latest_synced`. nornir re-exports the two
# pure predicates at `monitor::` and keeps the registry-typed
# `workspace_last_synced` wrapper (which delegates to `latest_synced`).
nornir-poll-cadence = { version = "0.1.0", path = "../edda/crates/nornir-poll-cadence" }
# nornir-release-core — the shared release READ MODEL (edda leaf): the
# `RepoGraph`/`DoctorReport`/`SkewStatus`/`DepPolicy` PODs, the pure graph theory,
# the dep-DAG, the semantic blast radius, the API-skew join, the portable gates,
# and the OBSERVING half of the cargo leg (`cargo_bin`, `changed_crates_since`,
# `impacted_crates`). Every `release::doctor` / `release::gate` re-export and every
# release viz pane reads through it.
#
# THIS REPLACES THE `dwarves` PATH DEP. nornir used to path-dep the forge for
# exactly this model — 102 `dwarves::` call sites — and that edge is why the forge
# could never compile nornir IN: `nornir -> dwarves -> nornir` is a cargo dependency
# cycle, which cargo rejects for normal deps regardless of features. The model
# belonged to neither half (L5a: a primitive a third consumer would want as-is), so
# it is a leaf both consume, and the edge is gone in this direction.
#
# nornir must NEVER regain a crate dep on dwarves. The suite graph is
# `dwarves -> nornir -> holger -> skade`; the forge is DOWNSTREAM of everything and
# is the one member that may embed siblings (see dwarves/.nornir/build-variants.toml
# `[build.downstream]`). Anything constructive nornir wants from the forge — a
# publish, a manifest edit, a pipeline run — the forge reaches IN to do, never the
# reverse.
nornir-release-core = { version = "0.2.0", path = "../edda/crates/nornir-release-core" }
# Móðguðr — the shared security VERDICT core (Engine A: multi-ecosystem
# CVE/license/provenance → Pass/Warn/Block `core::Verdict`). nornir is the
# consumer (viz Security pane + tripwire feed); holger consumes the same crate, so
# it lives in its own leaf repo to break the cycle (nornir → modgunn, never back).
# Features `warehouse` + `rustsec`: nornir is the advisory IMPORTER — it resolves
# the dependency closure (cargo metadata / sibling cloning) and writes parsed
# rustsec/OSV advisories into modgunn's shared `cve_advisories` table, then sources
# its verdict from that table (real severity/EPSS/KEV) rather than a local match.
# `warehouse` adds NO new dependency: nornir already path-deps the same `skade`
# 0.5 modgunn opens (see the `skade`/`skade-katalog` deps below) — both resolve to
# the one local checkout, so this opens the same physical catalog, not a second
# tree. See `src/security.rs` (the importer + the warehouse-sourced verdict).
# `scip` opts into modgunn's symbol-level vuln-sink + reachability-gate seam
# (`crate::scip::{VulnSink, reachability_gated_vulns}`, consumed by the b1
# reachable-vuln Mímir lens). It is an empty cfg feature (no extra deps) and is
# required because modgunn's always-compiled `warehouse::load_vuln_sinks` references
# `crate::scip` unconditionally — without it the `warehouse` build does not compile.
# `0.0.6`, not `0.0`: modgunn 0.0.5 on crates.io CANNOT BE BUILT — its
# `warehouse-sql` feature still named `skade/sql`, which skade dropped
# 2026-08-14, so `cargo metadata` failed for nine days and nornir's own publish
# verification died on it (E0615). The fix landed locally as f5870ca but was
# never released, so the registry kept serving the broken copy. A caret `0.0`
# would resolve straight back to it.
modgunn = { path = "../modgunn", features = ["warehouse", "rustsec", "scip"] , version = "0.0.8" }
# 📖 Manual-via-znippy (feature `manual-znippy`): pack `docs/book-svg/` → a
# content-addressed, seekable `book.znippy` and read pages O(1). Path deps on the
# sibling znippy workspace; optional so the default build doesn't pull arrow twice.
znippy-common = { version = "0.10.0", path = "../znippy/znippy-common", optional = true }
znippy-compress = { version = "0.10.0", path = "../znippy/znippy-compress", optional = true }
# Robust home-directory resolution: reads `$HOME`, falls back to getpwuid — so
# the nornir data root (`<home>/.nornir`) is correct for a login user AND for
# the systemd `nornir` service user (and `sudo -H -u nornir`). One rule, no
# nornir-specific path env-vars.
home = "0.5"
flate2 = "1"
tar = "0.4"
toml = "1"
toml_edit = "0.25"
anyhow = "1"
thiserror = "2"
# (Constant-time bearer-token compare moved to `nornir-service` (edda) — the
# server consumes it from there now, so nornir no longer deps `constant_time_eq`
# directly.)
# Multicore-friendly allocator: the deep-scan flush columnizes millions of small
# Strings across 32 threads — the stock glibc malloc serializes on its arena lock
# (only ~2x from 32 workers); mimalloc scales per-thread. Set as the global
# allocator in the server/CLI bins.
mimalloc = "0.1"
clap = { version = "4", features = ["derive"] }
# `json` powers `warehouse query --format json` (arrow → JSON rows via
# `arrow::json::ArrayWriter`); `prettyprint` backs the default table render.
arrow = { version = "59", features = ["json", "prettyprint"] }
parquet = { version = "59", features = ["arrow"] }
chrono = { version = "0.4", features = ["serde"] }
# Transitive dep (tantivy-common / typst-library / chrono). Pin away from the broken
# upstream time 0.3.48 — its new trait impl conflicts with StableDeref, breaking a
# fresh `cargo install` resolve. This direct entry only constrains the resolver;
# lift the upper bound once upstream fixes it.
time = ">=0.3.36, <0.3.48"
# Transitive dep of brotli 8.0.3 (via parquet 57). Pin to the 2.x line so the
# resolver never simultaneously picks alloc-no-stdlib 3.x (via brotli-decompressor
# 5.0.2 / alloc-stdlib 0.2.3, which widened to ">=2.0.4, <4") and 2.x (via
# brotli's own direct dep "2.0" = "<3"). Without this pin a fresh `cargo install`
# (no --locked) ends up with both 2.0.4 and 3.0.0 in the same compilation unit,
# whose `alloc::Allocator<u8>` impls are distinct types, producing ~36 E0277 errors
# in brotli/src/enc/writer.rs. The upper bound "<3" matches brotli's own constraint.
alloc-no-stdlib = ">=2.0.4, <3"
sha2 = "0.11"
uuid = { version = "1", features = ["v4", "serde"] }
# Copy-aside catalog.redb snapshot for lock-tolerant read opens when a live
# nornir-server holds the exclusive redb lock (warehouse::open_read_only).
tempfile = "3"
# The ed25519 SSH deploy-key mint now lives ONCE in `nornir-git::sshkey`
# (L5, DRY); `generate_keypair` is a thin wrapper over it, so nornir no longer
# needs direct `ssh-key`/`rand_core` deps here.
# Server-global workspace registry (one redb file at the server root). Same
# major as the catalog's redb so the tree carries a single version.
redb = "2"
# Pure-Rust SSH (no `ssh` subprocess) for git-over-SSH poll/push with the nornir
# deploy key. Re-exports `russh::keys` (ssh-key 0.6, same as ours).
russh = "0.65"
# znippy's gatling — the no-barrier worker-pool streaming engine. Fans the
# deep-scan's per-crate syn scans across all cores (see `src/deepscan.rs`) and the
# flattened knowledge scan's per-file syn parses via `gatling_forkjoin::
# gatling_map_balanced` (see `src/knowledge/symbols.rs`). 0.1.13 added that
# owned-item LPT map, replacing nornir's old inline mirror copy.
znippy-zoomies = "0.1.26"
# WebGPU compute for the GPU kNN (feature `vector-gpu`). Pinned to the version
# eframe already resolves, so the feature costs no new lock entries. LAW 4:
# WebGPU, never WebGL -- and here it is also the only GPU path that stays 100%
# Rust, because this box has a CUDA *driver* but no CUDA *toolkit* (no nvcc, no
# libnvrtc, no libcublas -- measured 2026-08-25), so a CUDA kernel would mean
# hand-written PTX behind FFI.
wgpu = { version = "30", optional = true }
pollster = { version = "1", optional = true }
bytemuck = { version = "1", features = ["derive"], optional = true }
# WebDriver client for the UI-robot tester (feature `robot`): drives a real
# browser through declarative TOML scenarios. See src/robot.rs.
# rustls, not fantoccini's default native-tls: native-tls links the SYSTEM openssl,
# which no build-toolchain image carries (MEASURED on loki 2026-10-16: the nornir,
# nornir-server and nornir-cli cells failed on `openssl-sys`). Our own TLS stack is
# rustls everywhere else.
fantoccini = { version = "0.22", optional = true, default-features = false, features = ["rustls-tls"] }
tantivy = "0.26"
walkdir = "2"
# The subclone lens's TOKEN lexer (`mimir_subclone::walk_tokens`) — a lexer, not a
# parser; `span-locations` gives each token its line. (`syn`/`quote` left with the
# syn knowledge scan, Mímir v2 §7.3b.)
proc-macro2 = { version = "1", features = ["span-locations"] }
cargo_metadata = "0.23"
inventory = "0.3"
# Pure-Rust HTTP client (rustls). Promoted from optional/docs-export to
# base so bench asset downloads don't require curl shellout.
ureq = { version = "3", default-features = false, features = ["rustls"] }
# Pure-Rust git implementation (no libgit2 / C deps). Used for
# release::publish::tag. Network features (push) deferred to a follow-up
# so we stay lean and avoid pulling reqwest/tokio just for tagging.
# `sha256` enables gix's SHA-256 object-format support (read+write of the local
# object DB + orphan-commit staging in docs::pages), so `docs publish-pages`
# works on sha256-format repos (e.g. tunnr) instead of failing at gix::open with
# `extensions.objectFormat=sha256 was invalid`. Local-only; the force-push stays
# external (the printed command), so no send-pack dependency is implied.
# gix 0.87, NOT 0.86: every `bisync` version was yanked upstream 2026-08-24, and
# `gix 0.86.0` -> `gix-protocol ^0.64.0` -> `bisync ^0.3.0` makes the whole 0.86
# line unresolvable from the registry (0.86.0 IS the line; `^0.64` cannot reach
# the fixed 0.65.1). The `tree-editor` FEATURE does not exist in 0.87 -- the tree
# editor is unconditional there -- so the list EMPTIES that entry, not shrinks it.
gix = { version = "0.90", features = ["status", "blocking-network-client", "blocking-http-transport-reqwest-rust-tls", "sha256"] }
# Offline vuln matching against a local rustsec/advisory-db mirror (airgap).
semver = "1"
gimli = "0.34"
object = "0.40"
memmap2 = "0.9"
rustc-demangle = "0.1"
tokio = { version = "1", features = ["macros", "rt-multi-thread", "io-std", "io-util", "sync", "fs", "time", "signal"] }
# Bridges russh's async channel I/O to the blocking Read/Write that gix's
# blocking git transport expects (SyncIoBridge), for SSH pack-fetch in `ssh.rs`.
tokio-util = { version = "0.7", features = ["io-util"] }
async-stream = "0.3"
rmcp = { version = "3", features = ["server", "macros", "transport-io", "schemars"], optional = true }
schemars = { version = "1", optional = true }
tracing-subscriber = { version = "0.3", features = ["env-filter"], optional = true }
tonic = { version = "0.14", optional = true, default-features = false, features = ["router", "transport", "codegen"] }
prost = { version = "0.14", optional = true }
tonic-prost = { version = "0.14", optional = true }
# Embedded holger (the /cache + /sparring server), pulled in-process by the
# `embed-holger` feature. Sibling path-dep; shares nornir's tonic 0.13 stack.
holger-server-lib = { version = "3.0", path = "../holger/server/lib", optional = true }
tokio-stream = { version = "0.1", optional = true }
# (typst-as-lib/typst-pdf/typst-svg/typst-html/typst, pulldown-cmark and
# rust-embed left with the docs code: the render engine lives in nornir-docbook,
# the svg/book/apidoc half in nornir-docgen. rust-embed had no remaining user.)
# Pure-Rust image codecs (PNG/JPEG/GIF/WebP). Only `viz` lights it now (the
# docs-export image work moved to nornir-docbook); the ui_snapshots round-trip
# test uses the dev-dependencies entry.
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "gif", "webp"], optional = true }
# Urðr Threads time-travel visualizer (feature = "viz")
# egui 0.35: MUST match facett, which nornir pulls by PATH under `viz`. When facett
# moved to 0.35 and this stayed at 0.34, one dependency graph carried two egui
# versions — `cargo tree -i egui` reported "ambiguous" and `--features viz` failed
# with 255 errors. eframe carries `wgpu`, never `glow` (LAW 4).
# (the themable GraphView arch board). egui-snarl 0.12 is the 0.36-compatible
# Funnel node-graph. Unifies the whole stack on one egui (no 0.33/0.34 split).
# LAW #4 — WebGPU everywhere, no OpenGL/WebGL: `wgpu`, never `glow`. With `glow` on and
# `wgpu` off, eframe resolved `Renderer::default()` to `Glow`, so this one word decided that
# the Urðr Threads viz rendered OpenGL with nothing in the source saying so. It also decided
# what the viz could DRAW: of the facett panes this viz embeds, graph3d owns a wgpu custom
# pass (via facett-core's graphcloud) that never ran under a glow host — it degraded to the
# CPU painter. (helix, warehousedeck and graphview own no wgpu pass; graphview is vello.)
# `src/bin/urdr-threads.rs` now names `Renderer::Wgpu` explicitly too.
# Enforced by tests/law4_no_opengl_backend.rs.
# 0.35, not 0.34: nornir pulls facett by PATH under `viz`, facett is on 0.35, and one
# graph carrying two egui versions made `cargo tree -i egui` ambiguous and `--features
# viz` fail with 255 errors. These two lines must track facett.
eframe = { version = "0.36", default-features = false, features = ["default_fonts", "wgpu", "wayland", "x11", "accesskit"], optional = true }
egui = { version = "0.36", optional = true }
# 🤖 WIRE-REMOTE robot-drive channel (feature `robot-remote`): the SAME reusable
# robot-UI tester the `robot_viz` reference test drives, but pulled as a real
# (optional) dep so the viz BINARY can MOUNT a `cmd_json::CmdJsonServer` over the
# viz shell on a Unix socket (the full-system-autotest fleet dials it). `wire`
# adds only the serde command/assertion channel — no eframe/windowing. Off by
# default; a stock `nornir` links nothing of it. (nornir already pulls this crate
# as a dev-dep for tests; Cargo unifies the two into one build.)
# PATH+VERSION, not path-only: cargo STRIPS `path` when it packages a crate for
# crates.io and then rejects what is left — "all dependencies must have a version
# specified when publishing" — so a version-less path dep makes `nornir` unpublishable
# outright, optional or not (cargo does not exempt optional deps). `0.3.0` is what is
# live (measured 2026-08-02: published max 0.3.0, byte-identical to edda's local copy,
# and its published manifest carries the `wire` feature this line asks for). A local
# build still resolves the sibling through `path`; the version only binds at publish.
nornir-robotui = { path = "../edda/crates/nornir-robotui", version = "0.4.0", optional = true, features = ["wire"] }
# The 🧬 nornir tab's job list is the reusable, nornir-agnostic facett component
# `facett-jobview` (Windows-Task-Manager-style). nornir maps `jobs::JobRecord →
# facett_jobview::JobEntry` and renders it. Pulls facett-core transitively; both
# are on egui 0.34, so there's no second-egui split. Published on crates.io.
facett-jobview = { version = "0.2.0", path = "../facett/facett-jobview", optional = true }
# The 📖 Manual tab renders nornir's own `docs/book-svg/` (the `nornir docs book
# --format svg` output) through `facett-docview` — a pure-Rust vector-SVG manual
# viewer (resvg/usvg, no pdfium). It parses + rasterizes pages multi-core (rayon).
# Same egui 0.34 family, so no second-egui split.
facett-docview = { version = "0.2.0", path = "../facett/facett-docview", optional = true }
# The 🚇 Metro tab renders nornir's button → gRPC → warehouse test-coverage
# CHAINS as transit lines through the reusable, nornir-agnostic facett component
# `facett-graphview::MetroView` (a `facett_core::Facet`). nornir maps its own
# `arch::metro::MetroLine` + `surface_coverage` lit overlay onto facett's
# `MetroMap` (see `viz::metro_feed`) and lets the Facet draw it. DEFAULT features
# only — no vello/wgpu (the CPU/egui reference path), so it stays build-light and
# on the same egui 0.34 / facett-core 0.1 as facett-jobview (no second-egui split).
facett-graphview = { version = "0.2.0", path = "../facett/facett-graphview", optional = true }

# The rotating 3-D funnel-plan view (sphere/helix/force) is the shared,
# nornir-agnostic facett component `facett_graph3d::Graph3D` (the 3-D-graph idea,
# generic). nornir's `viz::funnel3d::Funnel3D` is the thin adapter: it feeds a
# `PlanView` into `Graph3D` for the layout/projection math (deleting the formerly
# duplicated sphere/helix/force algorithms) and keeps nornir's themed painter +
# `state_json` surface. DEFAULT features only (CPU painter, no wgpu) — same
# egui 0.34 / facett-core 0.1 stack as the other facett facets.
facett-graph3d = { version = "0.2.0", path = "../facett/facett-graph3d", optional = true }
# 📊 Metric-bearing operator-DAG (the Spark SQL plan / Databricks Query Profile
# graph): the `facett_plandag::PlanDagView` Facet lays out a toposorted node DAG
# with per-node cost-heat, collapsible groups + selection. nornir's
# `viz::funnel_plandag` is the thin adapter feeding the funnel Plan-DAG
# (`funnel_core::PlanDag`) into it. DEFAULT features only — same egui 0.34 /
# facett-core 0.1 stack as the other facett facets.
facett-plandag = { version = "0.2.0", path = "../facett/facett-plandag", optional = true }
# 🏢 Warehouse Deck: a FacetDeck wall of N warehouse panes (3D graphs + grids +
# charts). Reuses Graph3D/Grid/PlotView; takes Arrow frames + view-models in.
facett-warehousedeck = { version = "0.2.0", path = "../facett/facett-warehousedeck", optional = true }
# 🧬 TIME-HELIX DAG view: the `facett_helix::HelixView` Facet renders a plan/dep DAG
# as a glowing helix advancing along a time axis. nornir feeds it a `Scene` built
# from each workspace's release timeline (releases along `t`, repos as coils) with
# the dep-graph snapshot's cross-repo edges as inter-helix links. DEFAULT features
# only (CPU painter, no wgpu) — same egui 0.34 / facett-core 0.1 stack as the other
# facett facets. Mirrors how facett-graphview is declared (path = sibling crate).
facett-helix = { version = "0.2.0", path = "../facett/facett-helix", optional = true }
# facett-core: the Elm/Facet contract + the HEADLESS HARNESS
# (`facett_core::harness::{snapshot_json, headless_render, drive}`) that the
# test-matrix uses to DRIVE each embedded facett component off-window and read
# back its Model — the seam that turns "6 embedded components" into "6 driven,
# snapshot-asserted surfaces". Pulled directly (not just transitively) so the
# `autonom::facett_probe` drive code can name `facett_core::harness`; same
# egui 0.34 / facett-core 0.1 stack as every embed above.
facett-core = { version = "0.3.0", path = "../facett/facett-core", optional = true }
# ⚒ Brokkr tab pipeline BUILDER: the reusable, nornir-agnostic facett component
# `facett_brokkr::PipelineBuilder` lets a user AUTHOR a `brokkr.toml` in the
# running viz two ways — raw TOML text or click-to-build stages/steps/triggers
# with a DAG preview. It emits `brokkr.toml` TEXT (no nornir edge); the Brokkr tab
# parses that text with brokkr's REAL `PipelineDescriptor` to validate + load it.
# Same egui 0.34 / facett-core 0.1 stack as the other facett facets.
facett-brokkr = { version = "0.2.0", path = "../facett/facett-brokkr", optional = true }
# facett-app: the DECLARATIVE UI-SCENE layer (`facett_app::scene`) — describe the
# whole master shell (menubar + a tree of foldable/draggable/drawer panes + nav
# routes) as serde DATA and render it through facett-app's OWN Router/TabView/menu
# primitives. nornir's Urðr-Threads shell is expressed as `viz::shell_scene` and
# rendered by `scene::render` (draggable master/detail split + a drawer action-log
# + a foldable Time-machine inspector, responsive → Tabs on a phone width). Same
# egui 0.34 / facett-core 0.1 stack as the other facett facets; see
# `.nornir/scene-design.md`.
facett-app = { version = "0.2.0", path = "../facett/facett-app", optional = true }
# 🏛/🧬 THE pan/zoom dependency-graph CANVAS: `facett_graphpan::{draw_graph,
# draw_graph_scrolled, GraphModel, Decorations, GraphView, content_extent,
# downstream_of}`. facett ADOPTED this verbatim from nornir's old
# `viz::graph_render` (its crate doc says so), so nornir's copy was a twin of the
# leaf that owns it (L5). `viz::graph_render` is now a thin re-export of this
# crate — the Architecture wiring board and the 🧬 release dashboard call THE
# canvas, and facett's legibility/GPU-cloud work lands here for free. DEFAULT
# features only (no wgpu) — same egui 0.34 / facett-core 0.1 stack as every embed
# above.
facett-graphpan = { version = "0.2.0", path = "../facett/facett-graphpan", optional = true }
# 🧪 THE pure test-matrix GRID model: `facett_matrix::{Matrix, RepoRow, Cell,
# CellStatus, …}`. facett adopted this verbatim from nornir's old
# `viz::test_matrix_grid`; nornir's copy is now a thin re-export (L5).
facett-matrix = { version = "0.2.0", path = "../facett/facett-matrix", optional = true }

# Iceberg warehouse (replacement for the hand-rolled parquet+rusqlite
# warehouse). Catalog backend is skade-katalog (sibling crate),
# a pure-Rust single-file ACID catalog with no SQL / no C deps.
# arrow-59 Iceberg via skade's vendored, separately published fork
# (skade/vendor/iceberg-arrow59 → crates.io `iceberg-arrow59`, library name
# `iceberg`, so every `use iceberg::…` here is unchanged). `path` = local dev
# against the sibling skade checkout; `version` = the published resolve. This
# replaces the old `[patch.crates-io] iceberg = {path=../iceberg-arrow59}` that
# crates.io strips on publish (which would have left nornir resolving arrow-59
# source against stock arrow-57 iceberg). skade is the single canonical
# publisher of the `iceberg-arrow59` crate name.
iceberg = { package = "iceberg-arrow59", version = "0.11.0", path = "../skade/vendor/iceberg-arrow59" }
# Floor 0.1.10 -> 0.1.20: `RedbCatalog::heal_all_tables` is not in any earlier
# published copy — E0599 in a Gate B verify build. 0.1.20 (today) is the first that
# has it, and it could not be published at all until skade dropped four expired
# znippy patches in its detached bench workspace that were holding six crates.
skade-katalog = { version = "0.2.0", path = "../skade" }
# skade: the iceberg writer/reader stack (ParquetWriter→DataFileWriter→fast_append
# + identity-partition keys) as one call. The warehouse delegates its write/scan
# mechanism here instead of hand-rolling it. default-features = false drops
# skade's `sql` (datafusion) — nornir has its own SQL surface.
# `>=0.4.12`: floor is the published API the warehouse consumes (append_with /
# read_all / commit_table / manifest writers — all stable since 0.4.12); the
# sibling checkout may be ahead (e.g. a 0.5.x WriteProps/bloom follow-on) and the
# `path` resolves to it locally, so accept any on-disk version at or above the floor.
skade = { version = "0.7.0", path = "../skade/skade", default-features = false }
# SQL over the warehouse (`nornir warehouse query "SELECT …"`). DataFusion is
# the execution engine; `iceberg-datafusion` (skade's vendored arrow-59 fork,
# lib name `iceberg_datafusion`) supplies the native `TableProvider` so each
# Iceberg warehouse table plugs straight into a DataFusion `SessionContext`
# with projection + filter pushdown to the Parquet scan — no MemTable copy.
# Same arrow-59 / iceberg-arrow59 universe as everything above, so the `Table`
# and `Catalog` types unify with `warehouse::iceberg`.
# THE ARROW ANCHOR, and why it is optional. datafusion 54.1.0 — the newest stable —
# hard-requires arrow ^58.3.0 and re-exports its own arrow. While it sat in the
# DEFAULT graph, nothing in the constellation could move to arrow 59: skade going to
# 59 gave 107 E0599 here (two icebergs) and edda 139 E0053 (the warehouse trait's
# signatures carry arrow types). Only `warehouse::sql::query` needs it — list_tables,
# describe_table, the formatters and the templates do not — so it is gated behind
# `sql`, OFF by default.
#
# datafusion 55 (crates.io) is the arrow-59 line — it superseded the
# apache/datafusion GIT rev skade pinned as "the only build on arrow 59", which
# is what had made this whole chain unpublishable. Both halves move together or
# the graph carries two `datafusion_session` majors.
#
# `version` AND `path`: path is what a local build resolves, version is what a
# published nornir resolves. Without the version, `cargo publish` refuses the
# whole crate ("all dependencies must have a version requirement specified"),
# which is why 0.5.4 shipped with this dep NON-optional against arrow 58 — and
# why knut's xtask suite could not compile against the published nornir at all.
datafusion = { version = "55", optional = true }
iceberg-datafusion = { package = "iceberg-datafusion-arrow59", version = "0.11.0", path = "../skade/vendor/iceberg-datafusion-arrow59", optional = true }
# lgz: pure-Rust parallel gzip decompressor (+ tar-entry extraction) — deep_scan
# reads each dependency's `.crate` gzip tarball and decompresses + extracts its
# `.rs` entries in ONE call, so a gatling worker chews a whole crate (fat CPU
# unit) instead of opening thousands of extracted files.
lgz = { version = "0.3.14", path = "../znippy-zoomies/lgz" }
async-trait = "0.1"
futures = "0.3"
# Embedding-model runtimes (optional; see the `embed-tract` / `embed-ort`
# feature comments). tract-onnx is pure Rust. ort runs in **load-dynamic** mode
# (set via the `embed-ort` feature → `ort/load-dynamic` → `ort-sys/disable-linking`):
# it links NO onnxruntime at build time and `dlopen`s `libonnxruntime.so` only at
# runtime when a GPU path is chosen — so a default build compiles & runs on CPU
# with no onnxruntime present. The `cuda` feature here just enables the CUDA EP
# bindings (no link under disable-linking); ROCm adds via the `embed-ort-rocm`
# feature. `tokenizers` pulls onig (C) — accepted exception. Its DEFAULT features
# also pulled esaxx (C++, `esaxx_fast`, the Unigram TRAINER's suffix array) and
# indicatif (`progressbar`), and nornir only loads and encodes (`Tokenizer`), so
# both are off (2026-09-26): nordisk-cargo-toolchain carries gcc and no `c++`, and
# dwarves' container build (MEASURED on loki) died in esaxx-rs's build script
# with `failed to find tool "c++"` after 11 min. esaxx-rs without `cpp` is pure Rust.
tract-onnx = { version = "0.23", optional = true }
# Exact pin: 2.0.0-rc.14 made `Session::builder` take an `&Environment` (rc.13: no argument), so no call compiles on both.
ort = { version = "=2.0.0-rc.13", default-features = false, features = ["std", "ndarray", "cuda", "api-24"], optional = true }
tokenizers = { version = "0.23", default-features = false, features = ["onig"], optional = true }
# Runtime dlopen of CUDA libs so the ort CUDA EP "just works" without a manual
# LD_LIBRARY_PATH (embed-ort only).
libloading = { version = "0.9", optional = true }
egui-snarl = { version = "0.12", optional = true }

# ── Generative-LLM backend runtimes (EPIC #39; all optional, all OFF by default) ──
# candle (`gen-candle`): pure-Rust tensor + transformer model zoo. The generator
# loads a quantized GGUF (qwen2 family) and runs a real sampling loop. No C deps.
candle-core = { version = "0.11", optional = true }
candle-transformers = { version = "0.11", optional = true }
# Model-weight fetch from the HF hub for the candle backend (quantized GGUF).
# 0.3 -> 0.5 (2026-10-02, W6): same `api::sync::Api` + `Cache` API; the old `online`
# feature is now `ureq` (ureq 3, rustls — no native-tls/OpenSSL). hf-hub 1.0 is an
# async rewrite on reqwest 0.13 (`HFClient`), i.e. a port of candle.rs, not a bump.
hf-hub = { version = "0.5", optional = true, default-features = false, features = ["ureq"] }
# mistralrs (`gen-mistralrs`): the candle-based in-process inference engine.
mistralrs = { version = "0.8", optional = true }

[dev-dependencies]
# The SCIP symbol grammar, as a TEST oracle only: `coderoom`'s
# `scip_monikers_round_trip_through_the_scip_crate` parses the monikers nornir
# builds with the reference parser. The `rust-analyzer scip` ingest it used to
# serve was removed with Mímir v2 (§7.3a).
scip = "0.10"
# `tests/viz_build_info_live.rs` stands up a REAL Flight server + in-mem store to
# prove the BuildInfoTail poller pulls live rows across intervals. It imported
# `nornir_flight` while the crate was never declared here, so that test had never
# compiled — found by the 2026-08-01 fleet compile sweep. Dev-only, and no cycle:
# nornir-flight deps only other edda leaves (nornir-service / -testmatrix /
# -bench), never nornir itself.
# path-only, no `version`: dev-deps are STRIPPED from a published crate, but a
# versioned path dev-dep is still RESOLVED by `cargo publish` and fail-stops when
# the sibling is not live. Named by `dwarves release_gates` (VersionedPathDevDep).
nornir-flight = { path = "../edda/crates/nornir-flight" }
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
uuid = { version = "1", features = ["v4"] }
# `tests/build_variants.rs::every_atom_satisfies_its_bins_required_features` reads
# the `[[bin]] required-features` gates straight out of the manifests — the roster
# is WALKED, never written down. Same version as the [dependencies] entry.
toml = "1"
# `tests/atom_linkage.rs` reads the built binaries' symbol tables through the
# SHARED `nornir_build_thing::linkage::LinkageAudit` (pure-Rust `object` parser,
# no `nm` subprocess) rather than re-implementing the audit — holger's
# `server/ui/tests/thin_client.rs` deleted its own copy of exactly this for the
# same reason (LAW 5).
nornir-build-thing = { path = "../edda/crates/nornir-build-thing" }
# Build the KNOWN `RgbaImage` the ui_snapshots round-trip test feeds through the
# WebP encode → warehouse store → fetch → diff path (dev-only).
image = { version = "0.25", default-features = false, features = ["png"] }
# Headless GUI testing — drives the real egui app via the AccessKit tree so the
# viz test matrix can click every button on every workspace and catch panics.
# No renderer feature: clicking walks the AccessKit node tree, no GPU needed.
egui_kittest = { version = "0.36", features = ["wgpu", "snapshot"] }
# The reusable robot-UI tester (EPIC ROBOTUI) — the `robot_viz` reference test
# drives the real viz through it and proves a robot run fills the test matrix.
# Dev-only: the published `nornir` crate doesn't depend on it.
# `snapshot` pulls the WebP-in-warehouse harness (`nornir_robotui::snapshot`)
# so the round-trip test can drive encode → warehouse store → fetch → diff.
# `watch` adds the live eframe WATCH backend so the `brokkr_watch` example can
# replay the dwarves/brokkr DAG scenario in a REAL window on DISPLAY=:0 (the same
# `Scenario` the headless `viz_brokkr_robot` test drives). Dev-only; nornir
# already links eframe for viz, so this only adds serde/toml.
nornir-robotui = { path = "../edda/crates/nornir-robotui", features = ["snapshot", "watch"] }

[build-dependencies]
# The proto codegen (`tonic-build` + `protox`) moved with the proto to the
# `nornir-wire` edda leaf; nornir's build.rs now only stamps the git identity and
# fetches embed-model weights.
# Model-weight fetch for the embed features (build.rs). Pure Rust.
ureq = { version = "3", default-features = false, features = ["rustls"] }
# Shared streaming sha256_file (edda leaf) — the build.rs model-weight hashing,
# consumed instead of a private read-loop + hex twin.
nornir-hash = { version = "0.1.1", path = "../edda/crates/nornir-hash" }

# ─────────────────────────────────────────────────────────────────────────────
# THE ATOMS — one `[[bin]]` per declared build atom (2026-08-01).
#
# ⚠ WHY THREE BINS SHARE `src/bin/nornir.rs` AND TWO SHARE `src/bin/urdr-threads.rs`.
# (The SOURCE file names are unchanged — the same call korp made when it renamed
# `korp` → `korp-ui` while keeping `path = "src/main.rs"`. Only launchables rename.)
# Until 2026-08-01 `.nornir/build-variants.toml` declared three variants (`nornir`,
# `nornir-thin`, `nornir-watch`) that ALL emitted `bin = "nornir"`, so the three
# feature builds wrote ONE path and the last one silently overwrote the others —
# nothing on disk said which you had. `BuildVariantSet::check_bins()` reported it:
#
#   3 variants emit the SAME artifact — bin 'nornir' (package nornir, profile
#   release, target <host>) … : nornir, nornir-thin, nornir-watch
#
# The fix is holger's: DISTINCT `[[bin]]` names off ONE source, told apart by
# `required-features`. `nornir::atom_line(env!("CARGO_BIN_NAME"))` (src/lib.rs) then
# names the atom from the BIN, so `nornir --version` and `nornir-thin --version`
# print DIFFERENT text — the atom selector is provably selecting, not decorating.
# It reads the bin name and NOT `cfg!(feature = …)` because cargo unifies features
# across every bin of one package in one invocation; see the ATOM MARKER note near
# the top of this file, and `.nornir/suite-atoms-design.md` §2.3.
# ─────────────────────────────────────────────────────────────────────────────

# ── client · fat — THE CLI (`nornir`). Not a server: the daemon is `nornir-server`.
[[bin]]
name = "nornir"
path = "src/bin/nornir.rs"

# ── client · mixed — THE CLI THAT REACHES holger. The same CLI source with
# `embed-holger` dropped. `atom-cli` is a pure marker feature (empty value): its
# ONLY job is to give this bin a `required-features` key of its own so it can
# coexist with `nornir` on disk. The atom NAME in `--version` comes from
# `CARGO_BIN_NAME`, not from this feature.
#
# RENAMED 2026-08-03: bin `nornir-thin` → `nornir-cli`, marker `thin` →
# `atom-cli`. The matrix names this row `nornir-cli`; the old names both claimed
# a linkage the bytes do not have (see `tests/atom_linkage.rs`).
[[bin]]
name = "nornir-cli"
path = "src/bin/nornir.rs"
required-features = ["atom-cli"]

# ── server · fat — THE DAEMON, and since 2026-08-03 also THE MCP SERVER.
# Declared by NO build variant until 2026-08-01, which is why the three CLI
# variants all called themselves `role = "server"`.
#
# ⚠ THE `nornir-mcp` BIN IS GONE (2026-08-03), the capability is not. The owner:
# *"mcp skall väl ingå inkompilerat i server!"* — `<x>-server` is `fat` = the
# service with everything compiled in, and MCP is a capability OF that service,
# not a delivery beside it. `src/bin/nornir-mcp.rs` moved into the library as
# `nornir::mcp_stdio` and this bin dispatches it:
#
#     nornir-server mcp     # the MCP stdio server (rmcp, transport::stdio)
#     nornir-server         # the gRPC daemon on :7878, unchanged
#
# Two subcommands of one binary, not two binaries, because MCP is stdio — an
# agent SPAWNS the process and speaks JSON-RPC over its stdin/stdout, which a
# long-running daemon holding a port cannot do. `mcp` therefore joins this bin's
# `required-features`, and `viz` is NOT decoration for it either: the MCP surface
# uses `nornir::viz::control::VizField` with no `cfg`. `required-features =
# ["mcp"]` alone was WRONG on the old bin and hid exactly that — `viz` is in
# `default`, so it only ever showed as a link error under `--no-default-features`.
[[bin]]
name = "nornir-server"
path = "src/bin/nornir-server.rs"
# NO `viz`. It used to be required here, with a comment naming the follow-up ("extract
# the pure-data summary out of the viz-gated module so a headless server can drop
# egui/wgpu") that nobody did — so the DAEMON could only be built with a window
# toolkit, and, worse, the `Viz` gRPC service was gated on that same GUI feature and
# therefore missing from every headless embedding of this lib. That is what left the
# forge answering `Unimplemented` to `Viz.Jobs` (see the note at `use pb::viz_server`
# in `src/server_grpc.rs`). The gate now sits on the ONE handler that genuinely reuses
# the egui-side crate (`Viz.Knowledge`, a named `Unimplemented` without `viz`), so the
# daemon builds and SERVES headless. `tests/viz_jobs_headless.rs` is the guard, and it
# can only run because this line dropped `viz`.
required-features = ["server", "mcp"]

# ── client · mixed — THE GUI. Renamed from `urdr-threads` on 2026-08-01 to match
# korp (`korp` → `korp-ui`) and dwarves (`dwarves-demo` → `dwarves-ui`); nornir was
# the last member without the uniform launch name. The PRODUCT name is unchanged —
# the window still says "Urðr Threads", `eframe::run_native` still takes it, and
# the app id is untouched. Only the launchable is renamed.
[[bin]]
name = "nornir-ui"
path = "src/bin/urdr-threads.rs"
required-features = ["viz"]

# ── solo · fat — THE APPLIANCE: the SAME GUI source with `nornir::server_grpc`
# started on a background runtime inside the process (see the `solo` cfg block in
# src/bin/urdr-threads.rs). Start it and the UI comes up with the server running
# inside itself: nothing else to install, no port to configure, no second process.
[[bin]]
name = "nornir-solo"
path = "src/bin/urdr-threads.rs"
required-features = ["solo"]

[[example]]
name = "vector_timetravel"
# Builds with either embedder backend; the example cfg-gates its body and
# prints a hint when neither `embed-tract` nor `embed-ort` is enabled.

# ── arrow-59 migration ────────────────────────────────────────────────────────
# ⚠ This block said "arrow-58" and `iceberg-arrow58` in every line until
# 2026-08-24, while the dep it describes has been `iceberg-arrow59` above. The
# migration it narrates HAPPENED; only the numbers were left behind. Corrected
# against the resolved graph (`Cargo.lock`: arrow 59.2.0, datafusion 55.0.0) and
# against the live registry, not against this comment's own memory.
#
# No `[patch.crates-io] iceberg` anymore. arrow-59 iceberg rides in as a real,
# uniquely-named, separately published crate: nornir's direct `iceberg` dep above
# is `package = "iceberg-arrow59"`, and the skade/skade-katalog path-deps depend
# on the SAME `iceberg-arrow59` crate — so the whole graph is arrow-59 with NO
# patch block that `cargo publish` would strip. The published nornir resolves
# `iceberg-arrow59` 0.9.10 (and skade >= 0.4.12 / skade-katalog >= 0.1.20) from
# crates.io, and — since 2026-08-24, with `sql` in `default` — also
# `iceberg-datafusion-arrow59` 0.9.9 and `datafusion` 55.0.0. All four were
# confirmed present on crates.io at exactly these versions the day this was written.
#
# Drop `iceberg-arrow59` everywhere the moment upstream iceberg-rust ships an
# arrow-59 release — then it's a plain `iceberg = "X"` version bump.

# DEV BRIDGE (remove before publish) — the `embed-holger` feature pulls holger's
# znippy-* repository crates, which now consume znippy-common's typed read views
# (`as_maven`/`as_rust`/`as_python`, znippy `feat/plugin-read-api`). A direct
# path-dep does NOT override a registry dep elsewhere in the graph — only [patch]
# does — so without this the embedded holger resolves znippy-common from crates.io
# (no views) and fails. Patches the whole graph to the local views checkout.
# [patch.crates-io] REMOVED IN FULL, 2026-08-23. All eight entries named their own expiry
# ("drop this line once it's on crates.io", "drop once 0.1.3 is the single published source
# both sides resolve", "drop once nornir itself resolves nornir-testmatrix from the
# registry") and every one has arrived. Checked against the LIVE registry — published ==
# local for all eight:
#
#   znippy-zoomies        0.1.24 == 0.1.24   (carries `gatling_map_balanced`, the
#                                             de-inlined knowledge-scan pool the first
#                                             paragraph was waiting for)
#   znippy-common         0.9.14 == 0.9.14
#   znippy-compress       0.9.13 == 0.9.13
#   znippy-decompress     0.9.11 == 0.9.11
#   znippy-plugin-maven   0.9.10 == 0.9.10
#   znippy-plugin-python  0.9.9  == 0.9.9
#   nornir-build-thing    0.1.8  == 0.1.8    (nornir-config's registry dep and ours are now
#                                             one source — the two-`BuildVariantSet` hazard
#                                             the old note describes is gone)
#   nornir-testmatrix     0.2.9  == 0.2.9    (facett-matrix's registry dep and ours unify —
#                                             `TestResultRow` is one type)
#
# All eight became true TODAY: znippy published 13 crates, znippy-zoomies 11, edda 16.
#
# Why this had to go rather than merely being tidy: `patch_fork_blockers`
# (nornir-release-core model.rs:189-195) treats a path/git patch target the repo does not
# itself produce as a FOREIGN FORK and holds everything transitively above it from
# crates.io. This block was holding `nornir`, `nornir-introspect` and `nornir-selftest` —
# the last thing between nornir 0.5.5 and the registry, AFTER Gate B had passed the
# rehearsal with no refusal at all.
#
# Fifth repo today where a patch block written as a local-dev bridge WAS the publish
# blocker: znippy 1 hold, holger 19, skade 6, nornir 3 — and gunnar's 3 still standing,
# because the gix-features fork has no published twin.

# The owner, 2026-09-05: test artifacts carry line tables only — full DWARF in every test
# binary and its deps was the bulk of a target dir (gunnar/target 271 GB, korp/target 852 GB
# on the day the disks filled); backtraces still show file:line.
[profile.test]
debug = "line-tables-only"

# ── nornir-build-thing / nornir-git: W3 crypto-bump bridge (2026-10-02) ────────
# The PUBLISHED nornir-build-thing pulls nornir-git on russh 0.61, whose
# `ecdsa =0.17.0-rc.18` cannot coexist with the stable RustCrypto set — the
# workspace does not resolve. edda's nornir-git is on russh 0.63 but unpublished.
# LOCAL-DEV bridge onto the sibling edda (a foreign-fork release blocker by
# `patch_fork_blockers`) — DROP it the moment edda publishes nornir-git /
# nornir-build-thing on russh 0.63.
[patch.crates-io]
nornir-build-thing = { path = "../edda/crates/nornir-build-thing" }
nornir-git = { path = "../edda/crates/nornir-git" }
# One znippy in the graph: nornir, modgunn and Skidbladnir take the sibling znippy
# by path, the holger crates take it from crates.io; both are 0.10.0, so these
# lines make them the same crate (2026-10-18, znippy 0.10). Stripped on publish.
znippy-common = { path = "../znippy/znippy-common" }
znippy-compress = { path = "../znippy/znippy-compress" }
znippy-plugin-python = { path = "../znippy/plugins/native/znippy-plugin-python" }
# ── holger's version-only sibling crates (2026-10-22) ─────────────────────────
# holger main (cbf89f0e) moved its sibling path deps into an optional
# `.cargo/estate-siblings.toml` include and now names these by version only. They
# are not on crates.io yet, so `embed-holger` (holger-server-lib) does not resolve
# without them. Same sibling checkouts holger's own estate include points at.
# DROP each line once that crate is published.
appliance-grow = { path = "../edda/crates/appliance-grow" }
nordisk-estate-svg = { path = "../monetize/crates/estate-svg" }
nordisk-twin = { path = "../Skidbladnir/crates/installer/nordisk-twin" }
nornir-auth-policy = { path = "../edda/crates/nornir-auth-policy" }
# One copy each: holger now names these by version only too, while nornir already
# builds them from the same siblings by path; without these lines crates.io copies
# land beside the path ones (two draupnir, two jera, ...).
draupnir = { path = "../draupnir" }
jera = { path = "../edda/crates/jera" }
modgunn = { path = "../modgunn" }
nornir-appshell = { path = "../edda/crates/nornir-appshell" }
nornir-auth-webauthn = { path = "../edda/crates/nornir-auth-webauthn" }
nornir-escape = { path = "../edda/crates/nornir-escape" }
nornir-humanize = { path = "../edda/crates/nornir-humanize" }
nornir-service = { path = "../edda/crates/nornir-service" }
nornir-zone = { path = "../edda/crates/nornir-zone" }