Nornir Warehouse — Authoritative Schema Reference
Epic A of THE_BIG_PLAN. Derived by a deep read of
src/warehouse/*plus an exhaustive grep of every Iceberg table name's write- and read-sites acrosssrc/. Section 2 documents only tables that exist in code today; Section 4 ("What should be added") is explicitly a proposal set.
0. How the warehouse is wired
The warehouse is an append-only columnar store. The canonical backend is IcebergWarehouse — Apache Iceberg via iceberg-rust over a single-file skade-katalog redb catalogue. Every derived fact is an Iceberg row; each append produces a fresh Iceberg snapshot, so the table "as of snapshot T" is the state at time T (true time-travel for free).
Key plumbing facts (from src/warehouse/mod.rs, iceberg.rs):
- Trait surface:
Warehouse(mod.rs) has 9 methods — the generic Arrow rail (append_arrow,scan_arrow,scan_filtered,scan_limited), schema plumbing (ensure_table,ensure_columns,table_names), and the two bench convenience methods (append_bench_run,query_bench_runs). Everything else is concrete methods / free functions onIcebergWarehouse. A futureRemoteWarehouse(Arrow Flight →nornir-server, Phase 5) will reuse the trait. - Open paths:
open()(mutating) vsopen_read_only()(copies a read-only snapshot aside if the live server holds the redb lock — so a CLI read can never be wedged). - Table registration:
IcebergWarehouse::opencalls anensure_layoutroutine (iceberg.rs) that creates the namespace if missing, then idempotentlycreate_table_if_missing/create_partitioned_table_if_missingfor every table below. New fields append with a fresh, unused field ID — never reuse, never renumber (iceberg_schema.rsheader rule). - Partitioning: artifact
*_snapshotspartition byrepo(lookup latest per repo); artifact*_blobspartition bysnapshot_id(fetch one snapshot's bytes); knowledge-map +sbom_components+knowledge_scanspartition byrepo. All others are unpartitioned (scan + filter / row-group pruning). - Generic readers:
table_names()andscan_preview(table, limit)back the viz Warehouse introspection tab and the server'swh.table_names/scan_previewRPCs — so every table is browsable generically even when it has no bespoke reader.
Type legend
Iceberg primitive types as they appear in iceberg_schema.rs. Timestamptz columns are stored as Arrow timestamp[µs, +00:00] (the ts_micros convention). req = required (non-null), opt = optional (nullable). "PK"/"FK" are logical — Iceberg has no enforced keys; they are the join columns the code actually uses.
1. Table inventory (72 tables)
Numbered 1..72 in ensure_layout (iceberg.rs) registration order — the row number is the physical table ordinal, so the last row is the total. Groups follow the create-order blocks.
| # | Table | Group | Partition | Schema fn |
| 1 | bench_runs |
bench | repo |
bench_runs() |
| 2 | bench_results |
bench | — | bench_results() |
| 3 | bench_telemetry |
bench | repo |
bench_telemetry() |
| 4 | test_outcomes |
bench/test | — | test_outcomes() |
| 5 | test_results |
test matrix (C6) | repo |
test_results() |
| 6 | build_provenance |
build provenance | repo |
build_provenance() |
| 7 | build_info |
build provenance | — | build_info() |
| 8 | dep_graph_edges |
dep graph | — | dep_graph_edges() |
| 9 | release_lineage |
release | — | release_lineage() |
| 10 | release_events |
release DAG | — | release_events() |
| 11 | release_changes |
release ledger (task #32) | — | release_changes() |
| 12 | funnel_events |
funnel DAG | — | funnel_events() |
| 13 | airgap_events |
airgap DAG (AIRGAP8) | — | airgap_events() |
| 14 | clone_events |
populate ledger (PLAN #6) | — | clone_events() |
| 15 | workspace_events |
populate step stream (#21) | — | workspace_events() |
| 16 | tantivy_index_snapshots |
artifact (code index) | repo |
artifact_snapshots() |
| 17 | tantivy_index_blobs |
artifact | snapshot_id |
artifact_blobs() |
| 18 | dwarf_snapshots |
artifact (stub) | repo |
artifact_snapshots() |
| 19 | dwarf_blobs |
artifact (stub) | snapshot_id |
artifact_blobs() |
| 20 | gimli_snapshots |
artifact (stub) | repo |
artifact_snapshots() |
| 21 | gimli_blobs |
artifact (stub) | snapshot_id |
artifact_blobs() |
| 22 | rustdoc_json_snapshots |
artifact | repo |
artifact_snapshots() |
| 23 | rustdoc_json_blobs |
artifact | snapshot_id |
artifact_blobs() |
| 24 | docs_index_snapshots |
artifact (docs index) | repo |
artifact_snapshots() |
| 25 | docs_index_blobs |
artifact | snapshot_id |
artifact_blobs() |
| 26 | doc_exports |
docs | — | doc_exports() |
| 27 | architecture_wiring |
architecture (ARCH n-006) | — | architecture_wiring() |
| 28 | embeddings |
vector | — | embeddings() |
| 29 | embedding_snapshots |
vector | — | embedding_snapshots() |
| 30 | embedding_manifest |
vector | — | embedding_manifest() |
| 31 | path_dep_audits |
cargo-pipeline F1 | — | path_dep_audits() |
| 32 | patch_strip_events |
cargo-pipeline F2 | — | patch_strip_events() |
| 33 | publish_attempts |
cargo-pipeline F3 | — | publish_attempts() |
| 34 | crate_metadata_checks |
cargo-pipeline F4 | — | crate_metadata_checks() |
| 35 | crate_tarball_stats |
cargo-pipeline F5 | — | crate_tarball_stats() |
| 36 | yank_events |
cargo-pipeline F6 | — | yank_events() |
| 37 | semver_diffs |
cargo-pipeline F7 | — | semver_diffs() |
| 38 | links_declarations |
cargo-pipeline F8 | — | links_declarations() |
| 39 | resolved_features |
cargo-pipeline F9 | — | resolved_features() |
| 40 | msrv_probes |
cargo-pipeline F10 | — | msrv_probes() |
| 41 | test_impacted_selections |
cargo-pipeline F11 | — | test_impacted_selections() |
| 42 | test_quarantines |
cargo-pipeline F12 | — | test_quarantines() |
| 43 | release_commits |
cargo-pipeline F13 | — | release_commits() |
| 44 | registry_mirrors |
cargo-pipeline F14 | — | registry_mirrors() |
| 45 | version_bump_plans |
cargo-pipeline F15 | — | version_bump_plans() |
| 46 | version_bump_targets |
cargo-pipeline F15 | — | version_bump_targets() |
| 47 | mcp_requests |
telemetry | — | mcp_requests() |
| 48 | vuln_findings |
security | — | vuln_findings() |
| 49 | warehouse_access_edges |
architecture (AUT7) | — | warehouse_access_edges() |
| 50 | surface_coverage |
autonom gate (n-005) | — | surface_coverage() |
| 51 | symbol_facts |
knowledge map | repo |
symbol_facts() |
| 52 | call_edges |
knowledge map | repo |
call_edges() |
| 53 | scip_occurrences |
knowledge map (resolved) | repo |
scip_occurrences() |
| 54 | feature_gate_facts |
knowledge map | repo |
feature_gate_facts() |
| 55 | test_inventory |
knowledge map | repo |
test_inventory() |
| 56 | git_heat_facts |
knowledge map | repo |
git_heat_facts() |
| 57 | sbom_components |
security | repo |
sbom_components() |
| 58 | knowledge_scans |
ledger | repo |
knowledge_scans() |
| 59 | coverage |
coverage (feature N) | repo |
coverage() |
| 60 | coverage_fn |
coverage (feature N) | repo |
coverage_fn() |
| 61 | ui_snapshots |
UI render oracle | repo |
ui_snapshots() |
| 62 | binary_facts |
binary analysis (#65 s1) | content_sha256 |
binary_facts() |
| 63 | section_sizes |
binary analysis (#65 s1) | content_sha256 |
section_sizes() |
| 64 | symbol_sizes |
binary analysis (#65 s1) | content_sha256 |
symbol_sizes() |
| 65 | artifact_diff |
binary analysis (#65 s2) | to_content_sha256 |
artifact_diff() |
| 66 | agent_model_runs |
bake-off (H5) | run_id |
agent_model_runs() |
| 67 | viz_actions |
viz action trail (N5) | session_id |
viz_actions() |
| 68 | saga_events |
saga record/replay (#25) | session_id |
saga_events() |
| 69 | infra_nodes |
yggdrasil infra | — | infra_nodes() |
| 70 | infra_edges |
yggdrasil infra | — | infra_edges() |
| 71 | runtime_spans |
yggdrasil runtime | — | runtime_spans() |
| 72 | feature_dep_edges |
knowledge map (manifest) | repo |
feature_dep_edges() |
Complete physical inventory: 72 tables (every
create_*_table_if_missinginensure_layout, cross-checked againstsql.rs's "71-table catalog" — that prose count predatesfeature_dep_edges). Of these, the five artifact pairs = 10 tables share just two schemas (artifact_snapshots()/artifact_blobs(), each reused by 5 tables); the remaining 62 tables each have their own schema fn. So the 72 tables resolve to 64 distinct schema functions — the artifact pair is the only schema reuse; all other rows map 1:1 to a fn. (This supersedes the earlier stale "47 tables / ~35 schemas" count, which predated the build-provenance, op/populate-event, coverage, UI-oracle, binary-analysis, bake-off, viz-trail, saga and yggdrasil-infra table families.)§2 caveat: the per-table documentation in §2 below is not yet exhaustive — it predates several of the families added here (binary-analysis, coverage, yggdrasil, etc.). §1 is the authoritative physical roster; §2 is a being-caught-up prose subset. Every table is browsable generically via
scan_preview/table_namesregardless of whether §2 documents it yet.
2. Per-table documentation
Group: bench / test history
bench_runs
One row per bench invocation (IcebergWarehouse::append_bench_run).
| col | type | n | key | notes |
| run_id | string | req | PK | UUIDv4 minted on append |
| repo | string | req | workspace repo name | |
| ts_micros | timestamptz | req | run time | |
| date | string | req | YYYY-MM-DD (denormalized for grouping) | |
| version | string | req | crate/tool version under bench | |
| machine | string | req | hostname; rejected if blank | |
| cores | int | req | CPU core count |
- Writes:
append_bench_run_async(iceberg.rs), invoked frombin/nornir.rs(nornir bench …). Co-writesbench_results+test_outcomesunder the samerun_id. - Reads:
query_bench_runs_async(filter by repo/machine/limit); vizviz/model.rstimeline;viz/timetravel.rs. - Relations:
run_id→bench_results.run_id,test_outcomes.run_id.
bench_results
Long/narrow metric rows for a run.
| col | type | n | key |
| run_id | string | req | FK→bench_runs |
| result_name | string | req | |
| metric_name | string | req | |
| metric_value | double | req |
- Writes: co-written by
append_bench_run_async. Reads: viz/timeline, genericscan_preview.
test_outcomes
One row per test case in a run; also the substrate for flake/quarantine windows.
| col | type | n | key | notes |
| run_id | string | req | FK→bench_runs | |
| test_name | string | req | ||
| passed | bool | req | ||
| duration_ms | double | opt | ||
| message | string | opt | failure detail | |
| source_repo | string | opt | impact-selection provenance | |
| source_crate | string | opt | impact-selection provenance | |
| attempt_idx | int | opt | flake retries | |
| retry_of_run_id | string | opt | FK→bench_runs | the run this re-ran |
| quarantined | bool | opt | auto-quarantine flag |
- Writes: co-written by
append_bench_run_async. - Reads: window queries feed
test_quarantinesdecisions; viz. - Relations:
run_id/retry_of_run_id→bench_runs;test_name+repo→test_quarantines.
Group: dependency graph
dep_graph_edges
Long format: one row per (snapshot, edge, via-crate). The whole cross-repo graph at a point in time. src/warehouse/dep_graph.rs.
| col | type | n | key | notes |
| snapshot_id | string | req | PK (with edge) | UUIDv4 per record_dep_graph |
| workspace_name | string | req | filter/pushdown key | |
| ts_micros | timestamptz | req | ||
| from_repo | string | req | consumer (empty = no-edge marker) | |
| to_repo | string | req | producer | |
| via_crate | string | req | crate justifying the edge |
- Writes:
dep_graph::record_dep_graph(one row per via-crate; empty-graph marker row if no edges). - Reads:
dep_graph::query_dep_graph_snapshots(pushesworkspace_namefilter + column projection, rolls long rows back intoDepGraphSnapshot);release::regression::traceranks suspects off a snapshot; viz Timeline RPC. - Relations:
snapshot_idis the join target forrelease_lineage,yank_events,semver_diffs,links_declarations,resolved_features,test_impacted_selections,version_bump_plans(all carrydep_graph_snapshot_id).
Group: release provenance
release_lineage
One row per (release, repo) — full provenance of a release op. Per-repo gate_status makes partial releases reconstructable. src/release/pipeline.rs (persist_lineage).
| col | type | n | key | notes |
| release_id | string | req | PK (with repo) | |
| workspace_name | string | req | ||
| ts_micros | timestamptz | req | ||
| dep_graph_snapshot_id | string | req | FK→dep_graph_edges | the graph it built against |
| build_order_idx | int | req | topo position | |
| repo | string | req | ||
| git_sha | string | req | ||
| git_branch | string | req | ||
| git_dirty | bool | req | ||
| gate_status | string | req | per-repo gate outcome | |
| tests_passed | int | req | ||
| tests_failed | int | req | ||
| dry_run | bool | req | ||
| published_versions_csv | string | req | crate@ver,crate@ver (empty on dry-run/fail) |
|
| tantivy_snapshot_id | string | opt | FK→tantivy_index_snapshots | time-machine pin |
| dwarf_snapshot_id | string | opt | FK→dwarf_snapshots | |
| gimli_snapshot_id | string | opt | FK→gimli_snapshots | |
| git_tag | string | opt | ||
| git_tag_matches_published | bool | opt | ||
| rustdoc_json_snapshot_id | string | opt | FK→rustdoc_json_snapshots | |
| patch_lines_stripped | int | opt |
- Writes:
release::pipeline::persist_lineage, invoked frombin/nornir.rs(nornir release). Reads:docs/release_book.rs(renders the per-repo lineage table), vizviz/model.rs+viz/timetravel.rs(release timeline, artifact pins for time-travel),release::regression. - Relations:
dep_graph_snapshot_id, the four*_snapshot_idartifact pins;release_idis FK target for the cargo-pipeline release-scoped tables.
release_events
The row-level DAG of a release run — one row per component × op × phase boundary as the pipeline walks the build order. Where release_lineage is the outcome, this is the shape. The structural twin of funnel_events. src/warehouse/release_events.rs (schema release_events()). Full design: release-events.md.
| col | type | n | key | notes |
| run_id | string | req | PK (with seq) | groups a run's rows; loosely ↔ release_lineage.release_id |
| seq | long | req | PK (with run_id) | per-run monotonic ordering key (Iceberg scan order is unspecified) |
| ts_micros | timestamptz | req | event time | |
| component | string | req | the component the op acts on (the repo today) | |
| repo | string | req | workspace repo name (== component today) | |
| op | string | req | run/test/bench/gate/publish/push/snapshot/… | |
| phase | string | req | start/end/skip | |
| status | string | req | ok/fail/warn/running | |
| detail | string | req | short message ("" = none, never null) |
|
| depends_on | list<string> | opt | dep-graph producers this op waited on; null on run/* rows, real (poss. empty) list on repo rows |
|
| elapsed_ms | long | opt | wall-clock duration; only phase=end rows |
- Writes:
release_events::ReleaseEventEmitter(best-effort + non-fatal), driven by BOTHrelease::pipeline::run_pipeline(library/server path) andrun_release_runinbin/nornir.rs(fat-CLInornir release run). The fat-CLI path keysrun_idto its ndjson progressrun_idso the live file and the warehouse DAG share an id. - Reads:
release_events::query_release_events(EventSelector::{Run|Repo|All})(sorted by(run_id, seq)); thenornir release events <repo|run_id>CLI (render_topo/rows_to_json); the future viz "Release Ops" tab (B3/B4). - Relations:
run_idloosely ↔release_lineage.release_id;depends_onentries +reposhare repo identity withdep_graph_edgesandrelease_lineage.repo.
Group: idea-intake funnel DAG
funnel_events
Append-only event log of the idea→plan→run DAG. Wide/typed (one optional column per variant field) so SQL readers query the DAG directly. src/warehouse/funnel.rs.
Envelope: event_id (string req, PK), ts_micros (timestamptz req), kind (string req — the variant tag).
Common id refs (subset per kind): idea_id, plan_id, node_id, run_id, from_node, to_node (all string opt).
Variant payload columns (all opt): source, text, refs (list<string>), decision, node_status, plan_status, why, summary, planner, node_kind, targets (list<string>), prompt_excerpt, params_json (string — open-ended JSON), ran_by, outcome, log_ref, produced_commits (list<struct{repo,sha}>), produced_test_runs (list<string>).
- Writes:
funnel::append_event(each mutation = one row = one Iceberg snapshot), driven bysrc/funnel/store.rsand the CLI funnel commands. - Reads:
funnel::load_all_eventsreplays the whole log into the in-memory DAG; viz Funnel tab; MCP funnel tools (bin/nornir-mcp.rs). - Relations:
run_idloosely co-references benchrun_id;produced_commitsreferences repo SHAs (same identity asrelease_lineage.git_sha,release_commits.git_sha).
Group: artifact time-machine (snapshot + blob pairs)
Five logical artifact kinds, each a (<kind>_snapshots, <kind>_blobs) pair sharing the generic artifact_snapshots() / artifact_blobs() schemas. Restore = SELECT blobs WHERE snapshot_id=X → write each (filename, bytes) to a tmpdir → mmap with the native loader (no re-tokenize/rebuild). Blob writes are content- deduped by sha256 (src/warehouse/blob_store.rs).
Kinds: tantivy_index_* (code search index — live), rustdoc_json_* (semver diff source — live, feature 7), docs_index_* (rendered-docs search index — live), dwarf_* + gimli_* (debuginfo / unwind — registered stubs, no capture logic ships yet).
<kind>_snapshots (schema artifact_snapshots, partition repo)
| col | type | n | key |
| snapshot_id | string | req | PK |
| workspace | string | req | |
| repo | string | req | partition |
| git_sha | string | req | identity |
| branch | string | req | |
| ts_micros | timestamptz | req | |
| schema_hash | string | req | |
| blob_count | int | req | logical blob count (pre-dedup) |
| total_bytes | long | req |
<kind>_blobs (schema artifact_blobs, partition snapshot_id)
| col | type | n | key |
| snapshot_id | string | req | FK→<kind>_snapshots |
| filename | string | req | |
| bytes | binary | req | payload |
| byte_len | int | req | |
| sha256 | string | req | dedup identity |
- Writes: snapshot rows by the capture path (index/docs build, rustdoc-json capture); blob rows via
blob_store::put_blobs_dedup. Idempotency:artifact_snapshot_id_for(repo, git_sha)skips re-capturing an existing SHA. - Reads:
index_snapshot_id_for/dwarf_snapshot_id_forlookups; restore paths in index/docs/semver; viz time-travel resolves the pins onrelease_lineage. - Relations:
snapshot_idis FK target from the matchingrelease_lineagepin column;rustdoc_json_snapshots.snapshot_idis referenced bysemver_diffs.from_snapshot_id/to_snapshot_id.
Group: rendered docs
doc_exports
Historized rendered-doc exports (inline bytes), one row per distinct (repo, doc_name, version, format, sha256). src/docs/warehouse.rs.
| col | type | n | key |
| export_id | string | req | PK |
| workspace | string | req | |
| repo | string | req | |
| doc_name | string | req | |
| version | string | req | |
| format | string | req | (md/pdf/…) |
| git_sha | string | req | |
| ts_micros | timestamptz | req | |
| sha256 | string | req | content identity |
| byte_len | long | req | |
| bytes | binary | req | full artifact |
- Writes:
docs::warehouseexport path (nornir docs export). - Reads:
list_doc_exports, doc fetch-by-version; the file directory underdocs/only holds the current copy, past versions come from here.
Group: vector / semantic search
embeddings
Content-addressed, deduped vector store. src/vector/store.rs.
| col | type | n | key |
| content_hash | string | req | PK (with model_profile) |
| model_profile | string | req | PK |
| dim | int | req | |
| vector | list | req | native Arrow f32 buffer → Vec<f32> |
| normalized | bool | req | |
| vector_sha256 | string | req |
embedding_snapshots
Identity of one index build (makes re-index idempotent).
| col | type | n | key |
| snapshot_id | string | req | PK |
| workspace | string | req | |
| repo | string | req | |
| git_sha | string | req | |
| branch | string | req | |
| model_profile | string | req | |
| chunker_hash | string | req | |
| fileset_hash | string | req | |
| complete | bool | req | |
| row_count | int | req | |
| ts_micros | timestamptz | req |
embedding_manifest
Per-occurrence rows for a snapshot (no vector, no source text).
| col | type | n | key |
| snapshot_id | string | req | FK→embedding_snapshots |
| ordinal | int | req | |
| content_hash | string | req | FK→embeddings |
| model_profile | string | req | FK→embeddings |
| file | string | req | |
| start_line | int | req | |
| end_line | int | req |
- Writes:
vector::storeindex path (append_embeddings+ snapshot + manifest). Reads: time-travel semantic search filters manifest bysnapshot_id, JOINsembeddings, builds the in-memoryVectorIndex. - Relations:
(content_hash, model_profile)joinembedding_manifest↔embeddings;snapshot_idjoinembedding_manifest↔embedding_snapshots.
Group: cargo-pipeline fact tables (features 1–15)
Status note (load-bearing for the gap analysis): these 16 tables are all registered at warehouse open and have full schemas + helper structs/prose, but only a subset are wired to a live write path today. The bespoke append code for
publish_attemptsandcrate_tarball_statsnow lives with the release ENGINE indwarves::release_doctor::publish(moved out of nornir under doctrine L5a —dwarves/crates/dwarves/src/release_doctor/publish.rs); nornir writeslinks_declarations(frombin/nornir.rs) and reads all of these back through the Mimir warehouse for its lineage verbs. The rest are forward-compatible schema stubs awaiting their producer. All are browsable viascan_preview.
| table | grain | key cols / FKs | notes |
path_dep_audits |
(release, repo, manifest_path, dep_name) | release_id, ts_micros | F1 audit version= on path deps; has_version, version_req, ok |
patch_strip_events |
per stripped line | release_id, repo, file, line | F2 [patch] strip log; raw_line, action |
publish_attempts |
per publish attempt | release_id, crate_name, version, attempt_idx | F3 index-propagation poll; index_seen_ts_micros, wait_ms, http_status, outcome |
crate_metadata_checks |
(release, crate@ver) | release_id | F4 readme/license/repo/description gate; booleans + license_expr, ok |
crate_tarball_stats |
(release, crate@ver) | release_id | F5 .crate size; tarball_bytes, largest_file*, exceeds_threshold |
yank_events |
per yank/unyank | event_id (PK), dep_graph_snapshot_id, cascade_root_event_id (self-FK) | F6 cascade yank in dep order |
semver_diffs |
per rustdoc-json compare | from_snapshot_id/to_snapshot_id → rustdoc_json_snapshots | F7 added/removed/changed items, suggested_bump |
links_declarations |
per links= decl |
dep_graph_snapshot_id | F8 conflict detection via GROUP BY links_value |
resolved_features |
per (member, dep_crate, feature) | dep_graph_snapshot_id | F9 resolved feature set, long/narrow |
msrv_probes |
per probe | probe_id (PK) | F10 actual MSRV via cargo +toolchain check; probe_status |
test_impacted_selections |
per selected test | run_id, dep_graph_snapshot_id, changed_repo | F11 audit of impact-selection choice |
test_quarantines |
per quarantine decision | test_name+repo | F12 flake decision state; pass_rate_window, sample_size |
release_commits |
per commit in range | release_id, git_sha, parent_sha | F13 conventional-commit parse for CHANGELOG; breaking, conventional_type, scope |
registry_mirrors |
(crate@ver, target_registry) | mirror_id (PK), release_id | F14 mirror publishes; outcome, bytes_copied |
version_bump_plans |
per coordinated bump plan | plan_id (PK), dep_graph_snapshot_id | F15 header |
version_bump_targets |
per crate in a bump | plan_id (FK), applied_release_id | F15 from/to version per crate |
- Relations: release-scoped tables FK
release_id→release_lineage; graph-scoped tables FKdep_graph_snapshot_id→dep_graph_edges;semver_diffsFKs intorustdoc_json_snapshots;version_bump_targetsFKplan_id→version_bump_plansandapplied_release_id→release_lineage.
Group: knowledge map (syn + gix producers)
One snapshot_id per nornir knowledge scan <repo>. Partitioned by repo.
symbol_facts
One row per public item found by the syn walker.
| col | type | n | key | notes |
| snapshot_id | string | req | PK (with item) | per-scan group |
| ts_micros | timestamptz | req | ||
| repo | string | req | partition | |
| crate_name | string | req | ||
| module_path | string | req | crate::a::b |
|
| item_kind | string | req | fn/struct/enum/trait/impl/mod/type/const/static/macro | |
| item_name | string | req | ||
| visibility | string | req | pub / pub(crate) / priv | |
| file | string | req | ||
| line | int | req | ||
| doc_lines | int | req | ||
| signature | string | opt |
call_edges
Call edges from syn ItemFn bodies (callee unresolved).
snapshot_id(req), ts_micros, repo(part), crate_name, caller_path, callee_ident, call_kind, file, line(int) — all req.
feature_gate_facts
One row per #[cfg(...)] on a discovered item.
snapshot_id, ts_micros, repo(part), crate_name, module_path, item_name, cfg_expr(raw text), file, line — all req.
feature_dep_edges
One row per edge of the FEATURE ACTIVATION graph parsed out of Cargo.toml — which feature enables which dependency / other feature. The manifest side of the feature story (feature_gate_facts is the source side). Exists because cargo metadata cannot supply it: its features map is already flattened and resolved, so the conditional structure — above all the weak pkg?/feat form — is gone before cargo hands the data over.
snapshot_id, ts_micros, repo(part), crate_name, feature, enables, kind, optional(bool), dep_kind — req; target_cfg — opt.
feature—""means UNCONDITIONAL (not behind any feature), i.e. the ⊥ "cannot be removed by any feature toggle" case. Not a missing value.kind—dep|feature|dep-feature|weak-dep-feature|implicit-dep.weak-dep-featureispkg?/feat, the ONLY form that creates an AND-group (fires only ifpkgis enabled by another route) — the reason the table exists.target_cfg— the[target.'cfg(...)']predicate; NULL ⇒ unconditional across targets (never the empty string).dep_kind—normal|build|dev, so a consumer can drop dev-deps the waycargo tree -e no-devdoes.- Sort key
(repo, enables, crate_name, feature)—enablesleads so the reverse point lookup "which features pull dep X" prunes files. - Writes:
warehouse::feature_dep_edges::append_feature_dep_edges, driven by populate step 7d (monitor::republish_with→compute_feature_graph), which parses every present member's manifests through thenornir-feature-graphedda leaf. Compute fans out over members ongatling; the append is serial and writes one snapshot id for the whole run (the writer groups byrepointernally, because the table is partitioned and skade allows one partition per append — a per-member id would make the reader return only the last member). Reads:query_feature_dep_edges— which errors (never returns an emptyVec) when the scope was never populated, because "no rows" here would read as the meaningful answer "nothing pulls this dependency". It also errors on akind/dep_kindstring it cannot classify rather than defaulting one. - One row type (L5): the row struct + the
EnableKind/DepKindcodecs come from the parser leaf (nornir_feature_graph, re-exported asnornir::feature_graph), so writer, reader and the in-memory graph cannot disagree about the shape. - Measured 2026-08-01 over the real nornir + edda + dwarves trees: 1078 edges / 86 packages, of which 8 are
weak-dep-featureand 661 unconditional.
git_heat_facts
Per-file git activity heat at scan time.
snapshot_id, ts_micros, repo(part), file, commits_total(long), commits_30d(long), commits_90d(long), authors_total(long), last_commit_ts_micros(timestamptz) — all req.
- Writes:
append_symbol_scan(symbol+call+feature-gate in one scan),append_git_heat_scan; driven bybin/nornir.rs(nornir knowledge scan) viasrc/mimir.rs. - Reads:
knowledge::query::load_latest(repo)(latest scan per repo, partition-pruned); vizcallgraph.rs,ops_tabs.rs,remote.rs. - Relations:
snapshot_idties the four tables of one scan together; recorded in theknowledge_scansledger keyed by(repo, git_sha).
Group: telemetry / security / ledger
mcp_requests
MCP tool-call telemetry (metrics only — no payloads).
ts_micros(timestamptz req), tool(string req), status(string req), latency_ms(long req).
- Writes:
append_mcp_calls(from the MCP serverbin/nornir-mcp.rs). - Reads:
query_mcp_stats(McpToolStataggregation); vizmcp_tab.rs.
vuln_findings
Cached OSV/RUSTSEC findings keyed by immutable crate@version.
crate(req), version(req), ids(req — comma-joined advisory ids; empty = checked-clean), summary(req), checked_at_micros(timestamptz req).
- Writes:
append_vuln_findings(src/security.rsafter an OSV/advisory-db lookup). Reads:query_vuln_findings(warehouse-first; only un-cached crate@versions hit OSV); vizsecurity_tab.rs; server RPC.
sbom_components
One row per resolved dep in a repo's closure, per capture snapshot_id. Partition repo.
snapshot_id(req), ts_micros(timestamptz req), repo(req, part), name(req), version(req), license(req).
- Writes:
append_sbom_components(src/security.rs, from a deep-scan /cargo metadatasweep). Reads:query_sbom_components(repo)assembles the CycloneDX SBOM from the latest snapshot per repo (no server-sidecargo metadata); vizsecurity_tab.rs.
knowledge_scans
Ledger: one row per (repo, git_sha) knowledge-scanned. Partition repo.
repo(req), git_sha(req), snapshot_id(req), scanned_at_micros(timestamptz req).
- Writes:
record_knowledge_scan. Reads:knowledge_scan_exists(repo, git_sha)makes re-scanning an unchanged SHA a no-op (idempotency on the republish path). - Relations:
snapshot_id→ the knowledge-map tables produced by that scan.
warehouse_access_edges
The fn → warehouse-table access fact (AUT7 / EPIC ARCH n-002): the ONE wiring edge no call graph yields — which function reads/writes which warehouse table. Code-derived by a static syn pass (src/warehouse/access_scan.rs), not a manual grep. Grounded in the layer-boundary-discoverability LAW: warehouse access goes through canonical accessors (query_<table> / append_<table> / the generic scan_preview), so the access sites are enumerable by callee name. One row per (caller_fn, table, access) site; one scan = one snapshot_id.
snapshot_id(req, PK with caller_fn+table+access), ts_micros(timestamptz req), caller_fn(req — crate::module::Type::fn), crate_name(req), table(req — the warehouse table, or the <dynamic> sentinel for a generic scan_preview(var, …) whose table is a runtime value), access(req — read|write), file(req), line(int req).
- Writes:
warehouse_access::append_warehouse_access_edges(one Iceberg snapshot per scan), fed byaccess_scan::scan_repo/scan_source. - Reads:
warehouse_access::query_warehouse_access_edges(AccessSelector:: {All|Snapshot|Table|Crate}), sorted byAccessEdge::key(); genericscan_previewfor the browser. This is the feeder the ARCH auto-generator (n-006) consumes for its core-fn → table edges (§5 ofarchitecture-wiring.md). - Accessor → table map (what's covered): the named per-table readers/writers (
query_test_results→test_results read,append_release_events→release_events write,record_dep_graph→dep_graph_edges write,append_bench_run→ bench_runs+bench_results+test_outcomes write,append_symbol_scan→ symbol_facts+call_edges+feature_gate_facts write,append_event→funnel_events write,persist_lineage→release_lineage write,load_all_events→funnel_events read,query_{mcp_stats,sbom_components,vuln_findings,viz_actions, agent_model_runs,bench_telemetry,dep_graph_snapshots}, the matchingappend_*,record_knowledge_scan/knowledge_scan_exists→knowledge_scans,load_latest→the four knowledge-map tables) plus the genericscan_preview/table_nameswith a literal table argument. - Not statically knowable (limits): a
scan_preview(var, …)with a runtime table →<dynamic>(the viz Warehouse browser + MCP RPC iteratetable_names()); accessor indirection through a fn pointer/closure; trait dispatch (TestSink::append) — matched by concrete fn/method name only.
3. ER diagram
Plain-text relationship list. Cardinality notation: 1──* = one-to-many, *──1 = many-to-one (required parent), *──o1 = many-to-optional-one. The joining key is in parentheses.
bench_runs 1──* bench_results (run_id)
bench_runs 1──* test_outcomes (run_id)
test_outcomes 1──* test_quarantines (test_name+repo)
dep_graph_edges 1──* release_lineage (dep_graph_snapshot_id)
dep_graph_edges 1──* yank_events (dep_graph_snapshot_id)
dep_graph_edges 1──* links_declarations (dep_graph_snapshot_id)
dep_graph_edges 1──* resolved_features (dep_graph_snapshot_id)
dep_graph_edges 1──* test_impacted_selections (dep_graph_snapshot_id)
dep_graph_edges 1──* version_bump_plans (dep_graph_snapshot_id)
release_lineage 1──* path_dep_audits (release_id)
release_lineage 1──* patch_strip_events (release_id)
release_lineage 1──* publish_attempts (release_id)
release_lineage 1──* crate_metadata_checks (release_id)
release_lineage 1──* crate_tarball_stats (release_id)
release_lineage 1──* release_commits (release_id)
release_lineage 1──* registry_mirrors (release_id)
release_lineage 1──* release_events (release_id≈run_id, loose)
release_lineage *──1 tantivy_index_snapshots (tantivy_snapshot_id)
release_lineage *──1 dwarf_snapshots (dwarf_snapshot_id)
release_lineage *──1 gimli_snapshots (gimli_snapshot_id)
release_lineage *──1 rustdoc_json_snapshots (rustdoc_json_snapshot_id)
tantivy_index_snapshots 1──* tantivy_index_blobs (snapshot_id)
dwarf_snapshots 1──* dwarf_blobs (snapshot_id)
gimli_snapshots 1──* gimli_blobs (snapshot_id)
rustdoc_json_snapshots 1──* rustdoc_json_blobs (snapshot_id)
docs_index_snapshots 1──* docs_index_blobs (snapshot_id)
rustdoc_json_snapshots 1──* semver_diffs (from/to_snapshot_id)
embedding_snapshots 1──* embedding_manifest (snapshot_id)
embeddings 1──* embedding_manifest (content_hash+model_profile)
version_bump_plans 1──* version_bump_targets (plan_id)
version_bump_targets *──o1 release_lineage (applied_release_id)
knowledge_scans 1──* symbol_facts (snapshot_id)
knowledge_scans 1──* call_edges (snapshot_id)
knowledge_scans 1──* feature_gate_facts (snapshot_id)
knowledge_scans 1──* feature_dep_edges (snapshot_id)
knowledge_scans 1──* git_heat_facts (snapshot_id)
feature_dep_edges *──* feature_gate_facts (repo+crate_name+feature≈cfg_expr, loose)
funnel_events 1──* bench_runs (run_id, loose)
4. What should be added (proposals — NOT in code today)
These are roadmap proposals. None of the tables/columns below exist in iceberg_schema.rs yet; each entry states why, the columns, and the write/read path it would plug into.
4.1 release_events — a clean release-op DAG ✅ IMPLEMENTED
Status: shipped (Epic B / B1+B2). Moved out of the proposal set — see the per-table entry in §2 (group "release provenance") and the design doc release-events.md. The shipped schema refined the proposal below: the PK is (run_id, seq) (a per-run monotonic seq is the durable ordering key instead of a bare event_id, since Iceberg gives no scan order); repo is a first-class column alongside component; detail is required ("" = none); elapsed_ms was added (nullable, end rows only); and depends_on carries component names (the dep-graph producers a repo waited on), not event ids — that's what the viz draws edges from.
Write path: release_events::ReleaseEventEmitter, driven by both release::pipeline::run_pipeline (server) and run_release_run (fat CLI), best-effort + non-fatal. Read path: nornir release events <repo|run_id> (CLI parity ✓) + query_release_events; the viz "Release Ops" tab (B3/B4) is the remaining consumer.
The original proposal (kept for the record):
| col | type | n | notes |
| event_id | string | req | PK |
| release_id | string | req | FK→release_lineage |
| component | string | req | repo / crate the op acts on |
| op | string | req | build/test/gate/package/publish/mirror/yank/tag |
| phase | string | req | start/end (or queued/running/done) |
| status | string | req | ok/fail/skipped/dryrun |
| ts_micros | timestamptz | req | |
| depends_on | list | opt | event_ids this op waited on (DAG edges) |
| detail | string | opt | short message |
4.2 test_results — first-class suite/case results ✅ IMPLEMENTED (C6b)
Status: shipped (Epic C / C6b). test_outcomes is bench-shaped (keyed to a bench_runs.run_id, co-written with a bench); this is the standalone green/red board the C6 test-matrix writes. Moved out of the proposal set — see the schema fn iceberg_schema::test_results(), write/read in src/warehouse/test_results.rs, and the design doc test-matrix.md. The shipped schema refined the proposal: the case column is named test_name; duration_ms is required (0.0 when the runner gives none); message is required ("" = none); status ∈ pass | fail | ignored | stalled (the stall watchdog records stalled + red); partitioned by repo so per-repo reads prune. There is no release_id pin yet (release-coupling is a follow-up; the matrix is deliberately bench/release-free).
| col | type | n | notes |
| run_id | string | req | PK (with suite+test_name) — one per nornir test run |
| repo | string | req | partition key |
| suite | string | req | crate / test target |
| test_name | string | req | module::path::test_fn |
| status | string | req | pass | fail | ignored | stalled |
| duration_ms | double | req | 0.0 when the runner gave none |
| ts_micros | timestamptz | req | shared across a run's rows |
| message | string | req | failure / stall detail ("" = none) |
- Write path:
nornir test run/test all→append_test_results(fat-CLI today, likebench run; aTest.SubmitRPC is a follow-up). One Iceberg snapshot per run;repo-partitioned so a batch is one repo. - Read path: the viz 🧪 Test pane (
src/viz/test_tab.rs); thenornir test history <repo>CLI (render_matrix/rows_to_json);query_test_results(TestSelector::{Run|Repo|All}).
4.3 Bench CPU / telemetry — bench_telemetry (IMPLEMENTED, P1.2)
Why. bench_runs had cores but no runtime telemetry, so a slow run couldn't be correlated with machine load. Telemetry is captured per bench unit, not per run, so it lands in a sibling table keyed by (run_id, bench) rather than as columns on the one-row-per-run bench_runs. (This supersedes the earlier "columns on bench_runs" sketch; per-bench grain is the right shape and it keeps the load profile out of bench_results so it never competes in the docs metric-comparison renderer.)
bench_telemetry (schema fn bench_telemetry(), partition repo):
| col | id | type | n | notes |
| run_id | 1 | string | req | FK → bench_runs.run_id |
| repo | 2 | string | req | partition key |
| bench | 3 | string | req | the BenchResult.name (PK with run_id) |
| n_cores | 4 | int | req | logical cores the sampler observed |
| cpu_pct_avg | 5 | double | req | aggregate CPU% across cores, avg over samples |
| cpu_pct_max | 6 | double | req | aggregate CPU%, peak |
| cores_busy_avg | 7 | double | req | cores >50% busy, avg |
| cores_busy_max | 8 | int | req | cores >50% busy, peak |
| mem_peak_mb | 9 | double | req | peak process RSS (MB, from VmHWM) |
| mem_pct_max | 10 | double | req | peak system mem utilisation (%) |
| elapsed_ms | 11 | double | req | wall time of the unit |
- Sampler:
src/bench/telemetry.rs— a 1 Hz background thread reading/proc/stat+/proc/self/status+/proc/meminfo(no perf/ptrace/crate); non-perturbing. Folds min/avg/max; publishes live cores-busy for the progress line. See.nornir/bencher.md§ Live CPU/mem telemetry. - Write path:
run_passinjects the summary into eachBenchResult.metricsunder reservedtelem_*keys (so it survives the stdout-JSON hop);append_bench_run_asynclifts those keys intobench_telemetryand excludes them frombench_results. - Read path:
IcebergWarehouse::query_bench_telemetry(Some(repo))→Vec<BenchTelemetryRow>; genericscan_previewfor the viz browser.
4.4 work_log — op / work-log table
Why. There is no durable, queryable record of who ran what nornir command when, and with what result across the whole tool. mcp_requests covers MCP tool calls only. An op-log gives provenance for every CLI/RPC mutation.
| col | type | n | notes |
| op_id | string | req | PK |
| ts_micros | timestamptz | req | |
| actor | string | req | user / agent / ci |
| command | string | req | e.g. release, knowledge scan, bench |
| args_excerpt | string | opt | redacted arg summary |
| workspace | string | req | |
| status | string | req | ok/err |
| duration_ms | long | opt | |
| produced_run_id | string | opt | links to bench/release/scan id it produced |
- Write path: a thin wrapper around CLI command dispatch in
bin/nornir.rs(and the server RPC layer) appends one row per mutating op. - Read path: viz "Activity" tab;
nornir logCLI; audit/forensics.
4.5 robot_test_results — UI/robot-test outcomes
Why. The Njord/UI robot-test goal (per memory) needs a place to land automated UI/robot test outcomes with screenshots/diffs, distinct from unit tests.
| col | type | n | notes |
| run_id | string | req | PK (with step) |
| scenario | string | req | named robot scenario |
| step | string | req | step / assertion id |
| status | string | req | pass/fail |
| ts_micros | timestamptz | req | |
| screenshot_sha256 | string | opt | FK→an artifact *_blobs capture |
| diff_pixels | int | opt | visual-diff magnitude |
| repo | string | req |
- Write path: the robot-test harness appends per step; screenshots stored via the existing
blob_storededup path into a newrobot_blobsartifact pair. - Read path: viz "Robot Tests" tab with pass/fail + screenshot preview; CLI
nornir robot status.
4.6 funnel_demo_provenance — funnel-demo provenance
Why. Funnel-demo / seeded-DAG runs currently leave no marker distinguishing demo events from real intake in funnel_events, so a demo can pollute the real DAG view. A provenance side-table (or a provenance column on funnel_events) lets the viz filter demo vs real.
| col | type | n | notes |
| event_id | string | req | FK→funnel_events |
| provenance | string | req | real / demo / seed / import |
| seed_id | string | opt | which demo seed produced it |
| ts_micros | timestamptz | req |
- Write path: the funnel-demo seeder tags every event it emits; real intake defaults to
real. - Read path: viz Funnel tab gains a "hide demo" toggle; queries can
WHERE provenance='real'. (Cheaper alternative: add one optionalprovenancecolumn tofunnel_events— field ID 28 — and skip the side-table.)
5. Verification notes
- Markdown is plain CommonMark; tables are GitHub-flavored.
- The entity/relationship diagram is a plain
textblock (ASCII tree, bare identifiers, relationships noted inline) — no mermaid, per the NUKE-MERMAID standard; it renders verbatim everywhere with no JS and no diagram engine. - This doc lives under
.nornir/and is intentionally not in thedocs-exportrender set, sonornir docs checkdoes not gate on it; it stays hand-maintained.
Migrations — use Iceberg schema evolution (LAW)
When a warehouse table's schema changes, use Iceberg's native schema evolution as much as possible instead of rewriting/repartitioning data:
- Add columns (new fields default to null on old rows), rename, reorder, widen types (int→long, float→double) — all are metadata-only operations in Iceberg; existing data files are untouched and stay readable.
- A reader written for the new schema reads old files transparently (missing columns → null); a reader for the old schema ignores new columns.
- Avoid full table rewrites, drop-and-recreate, or repartition-rewrite unless a change genuinely can't be expressed as evolution (e.g. a type narrowing or an incompatible partition-spec change) — and when unavoidable, do it as an explicit, logged migration, never silently.
- New tables/columns ship additively so a server running the old binary keeps reading the warehouse a newer binary wrote, and vice-versa.
On-append auto-evolution (ensure_table_schema)
create_table_if_missing is create-only — it never touches an existing table. So a table created by an older binary keeps that binary's (narrower) schema forever, even after the canonical schema fn grows new columns. A writer that builds the full canonical Arrow batch then fails with number of columns(N) must match number of fields(M).
The fix (shipped 0.4.24): [warehouse::iceberg::ensure_table_schema] is called on the write path right after load_table. It diffs the table's current top-level column names against the canonical schema; if any are missing it commits an Iceberg add-column migration (the missing fields added as OPTIONAL, keeping their canonical field-ids) via skade's RedbCatalog::commit_table raw primitive (AddSchema + SetCurrentSchema). iceberg-rust 0.9.1 exposes no public schema-mutation Transaction action, so this raw-commit path is the only public route until 0.10.
- Wired on:
agent_model_runs(append_agent_model_runs, 13→16) andtest_results(append_test_results, 8→10) — the two tables whose canonical schemas grew columns after live servers had already created them. The migration is logged to stderr and is a no-op fast path once the table is current. - Safe by construction: old data files are untouched (add-column is metadata-only; old rows read those columns back as null) and every read path already defaults the optional columns, so a mixed-version warehouse stays readable both ways.
Binary-analysis fact tables (task #65, slice 1)
The binary-analysis subsystem (the compiled-artifact twin of Mimir's source intelligence — .nornir/binary-analysis-design.md) adds 3 new Iceberg tables in slice 1, all keyed by content_sha256 — the build_info.content_id / build_provenance.digest join key every binary fact hangs off, so no new identity is invented. Written by nornir::binscan::persist_report (from nornir-introspect's size + hardening pure-Rust producers); registered + partitioned by content_sha256 in create_all_tables; read back by query_binary_facts / query_section_sizes / query_symbol_sizes.
binary_facts— one header row per analysed artifact: format / arch / class /total_bytes+ the checksec verdict (stripped / has_debug_info / pie / relro / nx / stack_canary / fortify / rpath / runpath / build_id_note) + the section/symbol/dep counts, plus therepo/git_sha/build_idbuild context. Partition + sort key:content_sha256.section_sizes— one row per (artifact, section):size_bytes/ vaddr / alloc·exec·write flags / file_offset / kind — the Bloaty section view. Sort key:content_sha256,section_name.symbol_sizes— one row per (artifact, symbol): mangled + demangled (± generics) names,crate_name(GROUP BY → per-crate roll-up), section,size_bytes, kind, binding,is_monomorphization(twiggymonos), and the DWARF-joinedsource_file/source_line. Sort key:content_sha256,crate_name,symbol_mangled.
Binary-analysis diff table (task #65, slice 2)
Slice 2 adds the 4th binary-analysis table, artifact_diff — the artifact-to-artifact diff, the binary twin of the source-side semver_diffs. One narrow row per change between a (base, new) artifact pair, keyed on the pair (from_content_sha256, to_content_sha256). Written by nornir::binscan::persist_diff (from nornir-introspect's diff pure-Rust producer — gatling-parallel over the union of both symbol tables, ROOT-LAW #0); partitioned by to_content_sha256 in create_all_tables; read back by query_artifact_diff(from, to).
artifact_diff— one row per change:kind∈size_delta/section_delta/crate_delta/symbol_added/symbol_removed/symbol_grew/symbol_shrunk/abi_added/abi_removed;name(the changed symbol / section / crate, ortotal_bytes),crate_nameattribution,before/after/delta_bytes(the size waterfall), andseverity(info/warn/high— a removed export ishigh, the gate hint). Plus therepo+ optionalrelease_idlineage anchor. Partition key:to_content_sha256; sort key:to_content_sha256,from_content_sha256,kind.
Deferred to later slices (do NOT exist yet): the sbom_components.derived_from column (slice 4). With artifact_diff, the live Mimir table count lands the design's 67→71 target (67→70 at slice 1; artifact_diff closes it at 71).