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

nordisk/nornir

.nornir/warehouse-schema.md

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 across src/. 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):

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_missing in ensure_layout, cross-checked against sql.rs's "71-table catalog" — that prose count predates feature_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_names regardless 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
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
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

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

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
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

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>).

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

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

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

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_attempts and crate_tarball_stats now lives with the release ENGINE in dwarves::release_doctor::publish (moved out of nornir under doctrine L5a — dwarves/crates/dwarves/src/release_doctor/publish.rs); nornir writes links_declarations (from bin/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 via scan_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

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.

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.

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).

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).

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).

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).

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).


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)

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

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

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

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

5. Verification notes


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:

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.


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-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).

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).