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

nordisk/dwarves

.nornir/incremental-republish.md

Incremental republish — rebuild only the blast radius

Plan item F1 of THE_BIG_PLAN. The server's monitored-workspace poll loop used to rebuild the whole workspace index over all members on any git change. F1 makes the rebuild incremental: when only some members' HEAD moved, it rebuilds only the changed members ∪ their transitive dependents (the Mímir affected set), not the whole workspace.

Why

A monitored workspace can hold many member repos. Today a single commit to one leaf forces a full re-walk + re-index of every member — wasted work that grows with workspace size. But the poll loop already knows which members moved (it diffs each member's last_seen_sha), and the republish already builds a cross-repo dependency graph (cargo metadata --no-deps) to persist for the viz Dep Graph tab. F1 simply consults that graph: a change to a member can only affect that member and the members that (transitively) depend on it. Nothing else needs reindexing.

This is the graph-scoped sibling of the SHA-idempotency optimization already in republish_with step 1 (skip the knowledge scan of a member whose HEAD SHA was already scanned). F1 adds the same "rebuild only what moved" idea to the code-index rebuild (step 2), driven by the dep graph rather than per-member SHA equality.

The affected set

The blast radius of a change is computed by WorkspaceGraph::affected_by_change:

affected(changed) = ⋃ over c ∈ changed of  { c } ∪ dependents_transitive(c)

returned in build order (dependencies first). Edges point consumer → producer, so dependents_transitive(util) walks the reverse edges to find every member that would have to be rebuilt because util changed.

Worked example — the diamond app → liba → util, app → libb → util:

changed affected set skipped
liba {liba, app} libb, util
util {util, liba, libb, app} — (everything depends on the leaf)
app {app} liba, libb, util (nothing depends on the root)

monitor::affected_set wraps affected_by_change and intersects the result with the present member checkouts — a changed member that has no materialized .git checkout (SSH-only, not yet clonable) or a graph node that isn't a real workspace member is dropped, so the index scope only ever names real member directories.

Wiring

poll sweep (sync_once / nornir-server sweep)
    │  rep.changed = members whose HEAD moved this tick
    ▼
republish(reg, root, ws, changed: Option<&[String]>)
    ▼
republish_with(.., members, ws, deep_scan, changed)
    │  1. build cross-repo dep graph (cargo metadata --no-deps) — up front
    │  2. scope = match (changed, graph):
    │        (Some(non-empty), Some(graph)) → affected_set(graph, changed, present)
    │        _                              → []   (= full rebuild)
    │  3. idx.build_scoped(&scope)          ← only the affected member dirs walked
    ▼
Index::build_scoped(scope) → walk_workspace(root, scope)

Index::build_scoped is the new entry point: build() delegates to it with the index's configured repo_scope; the incremental path passes the affected set instead. An empty scope walks the whole workspace — identical to an unscoped full build() — which is exactly the fallback behavior we want.

Fallbacks (always safe to over-build)

A scope of [] means "full rebuild". We fall back to it whenever scoping would be unsafe or meaningless:

The graph is built once, up front and reused for the step-4 persist (it was previously rebuilt there), so incremental scoping costs no extra cargo metadata invocation.

What is not yet incremental

Tests

monitor::tests::affected_set_is_changed_union_dependents_and_skips_unaffected (inject-assert, LAW 1): feeds a fake change-set + a real diamond WorkspaceGraph and asserts the affected set equals changed ∪ transitive dependents, that an unaffected sibling/dependency is skipped, that the result comes back in build order, and that a changed member with no checkout is dropped by the present-intersection.