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

nordisk/facett

.nornir/open-items-2026-09-18-plan.md

Open items after the 2026-09-18 session — the plan

nornir:class=design

Written at the end of one long session (facett Streamlit demo → build-thing rows → runa pipeline → GPU graph layout in the browser → korp on loki). Everything below is something that session FOUND and did not finish. Each item says what is known, what is only suspected, the first concrete step, and how to tell it is done. Ordered by what unblocks the most, not by size.

What is already done and on main is not repeated here — see facet-wrapped-wheel.md row 13, gpu-prep-in-the-browser.md, and the runbook's §0a.

1. funnel-core has no repository — BLOCKS every fresh checkout

Known. edda/crates/dwarves-runa path-deps ../../../funnel-core. ssh://git@codeberg.org/nordisk/funnel-core.git answers Cannot find repository, although the published crate's metadata names that URL, and facett's [build.toolchain].siblings lists it — so the forge expects to clone it. On loki it exists only because it was unpacked from the crates.io cache (funnel-core-0.1.4.crate), which is what facett's Cargo.lock pins as a path dep.

Step. Push the real funnel-core source to nordisk/funnel-core (Rickard — it needs the source of truth, which is not on this box; the unpacked crate has a normalised Cargo.toml and no history). Same class, already solved a different way: tunnr lives under mikronova, not nordisk — worth a line wherever siblings are listed.

Done when git clone ssh://git@codeberg.org/nordisk/funnel-core.git works and a fresh checkout of facett builds with no manual step.

2. facett_map3d_water does not compile in any browser — DONE 2026-09-19

Closed. water.wgsl now samples unconditionally (clamped UV) and picks with select; the guard is xtask/tests/wgsl_compiles_in_the_browser.rs. It runs every .wgsl in the tree through headless Chrome's validator (Tint) — headless is VALID for this, unlike for rendering: shader validation needs no canvas and no readback, and SwiftShader's Tint is the RTX's Tint. Measured red→green on the same box: the old shader fails at 135:24 'textureSample' must only be called from uniform control flow, the new one passes, and the guard first compiles a five-line copy of the defect and demands Tint REJECT it, so a validator that went lenient would fail the test rather than blind it. naga 29.0.4 with ValidationFlags::all() accepts the old shader, checked the same day — which is why no native test could carry this. Bonus finding: facett-map/src/gpu/line.wgsl only compiles composed behind common.wgsl (it calls coverage_inside), which is how facett_core::render::wgsl builds it; the guard retries composed and says so per file.

Was known. Chrome's WGSL validator (Tint), measured on the built bundle: 'textureSample' must only be called from uniform control flow at the reflection branch — if (ruv.x >= 0.0 && ruv.x <= 1.0 && …) { refl_col = textureSample(…) }. naga accepts it natively, so no native test sees it. The map3d_water_pipeline is therefore INVALID on the web: water does not draw, and the error is only in the browser console.

Step. Sample unconditionally and select afterwards — let s = textureSample(refl_tex, refl_smp, clamp(ruv, 0, 1)); refl_col = select(fallback, s.rgb, in_bounds); — or use textureSampleLevel, which has no uniformity requirement. Then hold it with a guard that does not need a browser: run every .wgsl in the tree through a validator that enforces uniformity analysis (naga's does not by default; a Tint/dawn CLI check in the toolchain image would). That guard is the real deliverable — this is the second browser-only defect the same evening.

Done when the console of a headful Chrome run (recipe: memory note webgpu-browser-probing-on-loki, or row 13) shows no Invalid ShaderModule, and the guard fails on the old shader.

3. The gh-pages demo selects GPU lanes and installs none — DONE 2026-09-19

Closed. wasm_start now calls wasm_install_gpu_lanes(rs) from its wgpu_render_state block — map, map3d, graph cloud, and the layout-device loan — and the declared wasm axis (DEMO_WASM_FEATURES, one writer, build-variants.toml re-emitted) grew gpu and graph-layout-gpu, the same two lanes the wheel selects. xtask/tests/wasm_bundle_installs_the_lanes_it_selects.rs gained two tests that read that axis against the function body, both directions (a selected lane with no install; an install for a lane not on the axis), and refuse a fifth axis entry the guard does not know. NOT yet proven in a headful browser — that is item 7's robot.

Was known. facett-demo/src/main.rs::wasm_start builds the app with no install_renderer / install_cloud_renderer / install_layout_device calls at all. Its comment still says "No wgpu RenderState on the glow web path", but the pages build has used --no-default-features --features map-wgpu,map3d-wgpu for a while (now declared as DEMO_WASM_CARGO_ARGS). This is the select-without-install defect that facet-wrapped::install_gpu_lanes and its guard test were written to end — alive in the sibling entry point.

Step. Mirror the native block inside the wasm creator (if let Some(rs) = cc.wgpu_render_state.as_ref() { … }), add graph-layout-gpu to the declared wasm axis in facett-build-thing (one writer — the xtask reads it), and extend wasm_bundle_installs_the_lanes_it_selects.rs — or a twin of it — to cover facett-demo's manifest and wasm_start.

Done when the pages demo's state hook reports the map3d lane as GPU in a headful browser, and the guard is red if an install line is removed.

4. korp-server --env KEY=VALUE never reaches the unit

2026-09-19 addendum. korp-server-gpu is now installed SYSTEM-scope on loki (korp-server --install --system: unit /etc/systemd/system/korp-server.service, User=korp uid 118, state root /home/korp/.korp, config read from /home/korp/.config/korp/config.json). The user-scope unit from 2026-09-18 is disabled. Two more installer facts, read in the code: --run-as and --env are parsed but never passed to run_install (korp-server.rs ~L715, service.rs ~L1373 hardcodes run_as = Some(SERVICE_USER)), so the env defect has a second cause upstream of skidbladnir. And systemctl stop mid-convert SIGSEGVs the process in lbzip2::chunk::decode_segment_inner (coredumpctl, twice) — a separate defect.

Known (from the korp rebuild on loki). Two defects: the space form --env KEY=VALUE after --install is swallowed by the POINTER parser ("no executable at KORP_PLANET_CACHE=…"); and even --env=KEY=VALUE, which operator_env_from_args accepts and korp_server_spec_with_env passes to skidbladnir, produces NO Environment= line in the written unit — on --install and on --refresh. Worked around with ~/.config/korp/config.json (planet_cache_dir).

Suspected, not verified: the pairs are dropped inside skidbladnir's unit renderer rather than in korp. Nobody has read that code yet.

Step. A failing test first: render a spec with one env pair, assert the unit text contains Environment=K=V. Then fix whichever side drops it, and make the POINTER parser skip --env's value.

Done when korp-server-gpu --install --user --env K=V writes the line and systemctl --user show korp-server -p Environment shows it.

5. Other hosts should lend the layout device — korp DONE 2026-09-19, the rest do not apply

Closed for korp. korp e2cae68: feature graph-layout-gpu = ["facett-graphnav/gpu"] on the four GPU rows (off for korp-ui-cpu), recipe re-emitted, stamp names it, and main.rs lends the device beside the other three installs. Built through the declared door; NOT restarted on the desktop and NOT yet observed (layout_lane == "gpu-job" on the 89 130-node road graph is the proof, and it needs a korp-ui restart). dwarves-gui and holger-ui do not mount GraphNav (grepped: no facett-graphnav edge in either tree; dwarves-gui installs only the cloud renderer), so the row's claim that they take the blocking arm was wrong — there is nothing to lend there. Two pre-existing reds in korp's variant_shape test are recorded in that commit.

Was known. facett_graphnav::install_layout_device(&rs.device, &rs.queue) is the one call that puts large force layouts on the GPU as a non-blocking job. facett-demo and the wheel make it. korp-ui, dwarves and holger-ui mount GraphNav and do not — so with graphnav's gpu feature on they take the BLOCKING arm (second device, a wait on the paint path), and with it off, the CPU lane.

Step. One line beside each host's existing install_* calls + enable facett-graphnav/gpu. korp first: it is the host that opens 89 130-node road graphs.

Done when korp-ui's state dump shows canvas.layout_lane == "gpu-job" for a graph over 2 000 nodes.

6. The demo's live panes have never been proven

2026-09-19, later — the Graph pane was lying about an empty graph. Against a FalkorDB holding 1 212 122 nodes (seeded with the fat korp demo), the pane and korp-ui both said korp-graph-9 … holds 0 nodes. Cause: korp-server's live query path (knut-graphdb FalkorBackend::cypher → run) decodes exactly ONE result shape, the scene tail (id(a) AS src, id(b) AS dst, type(r) AS rel, … slon/slat …); the pane sent MATCH (n)-[e]->(m) RETURN n,e,m LIMIT 200, which decodes to zero rows, and the server reports zero rows as an EMPTY GRAPH. Empty query → the server's own scene read → 313 nodes / 300 edges. facett side fixed: client.py and graph_pane.py default to the blank query and the pane's help names the required RETURN columns; the headful robot now mounts graphnav (layout=cpu, 313 nodes is under the 2 000-node GPU threshold, no fallback). korp side still open: a query whose RETURN shape the decoder cannot read must be answered as "unreadable shape, expected these columns", not as korp-graph-9 — that verdict claims a fact about the graph that is false. Not fixed; it is knut-graphdb's decoder plus korp's object.rs Ok(_) => NoScene::Empty arm.

2026-09-19 addendum. Per Rickard, the planet is now fetched FROM SCRATCH by the system-scope service (planet_cache_dir = /mnt/work4t/korp-cache, empty at start; the complete planet in /mnt/work4t/osm-cache is kept as a backup and not referenced). The fetch is a korp_planet_fetch job in the server's ledger (korp.KorpControl/JobLedger, grpcurl), running beside korp_region_provision. Measured ~7 MB/s → the 166 GiB takes hours, then the 31–52 min convert. Also: the system service REFUSES the rootless draupnir-korp-falkordb / -spark-iceberg containers on :6379/:15012 (korp-backend-7), so the FalkorDB-load step will block until those are stopped or the backend rows say start = no. Not touched.

Known. test-demo is 51 passed / 5 skipped; the skips are 3 live panes wanting korp-server on :50051 and 2 saga tests wanting a sink on :7878. korp-server now runs on loki, but when the suite met it mid-provisioning the panes failed honestly ("could not read the region GeoParquet", redis refused) — so they need a server that has FINISHED provisioning Sweden and has FalkorDB up, not merely a listening port.

Step. After the planet→Sweden convert and the FalkorDB image build finish, run cargo xtask test-demo and read the three live-pane results. Whatever fails then is real.

Done when the three korp-server not reachable skips are passes.

7. The render robot, and a browser that can run it

Known. py/tests/robot_streamlit.py (playwright) is the only test that drives the real componentReady/RENDER_EVENT handshake; it has never run here. And headless cannot be the vehicle: headless Chrome offers only SwiftShader, cannot present a WebGPU canvas, and rejects wgpu map_async readbacks — it reports working GPU lanes as broken (row 13).

Step. A robot image with playwright + Chrome, run HEADFUL against a virtual display (Xvfb/weston) with --enable-unsafe-webgpu --enable-features=Vulkan; assert on facet_state() — including canvas.layout_lane — not on pixels. Add it as a stage after demo in facett.runa.ron.

Done when the robot is green in the pipeline and red when a lane install is removed.

8. facett.runa.ron has never been executed

Known. The pipeline parses, validates and names only declared variants and real verbs (guard test). No dwarves binary is built on loki, so DWARVES_REPO_ROOT=$PWD dwarves run facett.runa.ron has not run once.

Step. Build dwarves, run it. Expect the Build(variant: "facet-wrapped-web") stage to be the interesting one — it needs the forge's package builder and the pinned toolchain image digest to exist on the box.

Done when one full green run is recorded.

Small, and should not be forgotten

9. FDEB edge bundling on the GPU — DONE 2026-09-19 (lane only, no caller)

facett-graphview/src/fdeb_gpu.rs + fdeb.wgsl (gpu feature). Parity with the CPU lane held by a test on a real adapter; measured 2.46 s → 30 ms at 500 edges and 38.2 s → 212 ms at 2 000 edges, 60 iterations, RTX 5080. Blocking readback, so native only; no view calls it yet. See gpu-prep-in-the-browser.md.

6, continued — 2026-09-19 evening: Map and Graph panes LIVE, Population and saga still honest-empty

Done today for item 6: Sweden provisioned from a fresh torrent (7 of 7 steps), korp-server owns its own FalkorDB and Spark+Iceberg under uid 118 (Rickard's rootless draupnir-korp-falkordb / -spark-iceberg were stopped so the server would start its own; both images were built through draupnir's OCI engine, FalkorDB ~5 min). test-demo is 53 passed / 1 failed / 2 skipped; the headful robot mounts 9 frames — osm2d (Sweden, live) and graphnav (server scene read, 120 nodes) among them. Two facett fixes on the way: the demo client's 4 MB gRPC receive cap (a lod-12 tile is 6.75 MB) and the Graph pane's query shape (korp-graph-11 on the server side).

Population pane (stat-grid) — still no_tables, and the cause is korp's lake wiring, not the pane. IcebergTables/Population on the server resolve the warehouse to /home/korp/.korp/demo_warehouse and, being WarehouseBinding::Live, read it through open_row(lake_row("")) — the Spark/Iceberg REST row on :15012. The seed written INTO that directory (fat korp demo --warehouse … as user korp: 12 tables, skade local catalog) is not what that arm lists; and the server's own Populate RPC reports stage: done with backend skade AND with backend spark41 (scale 1e-6, drop) yet IcebergTables stays 0 and Population says no_tables. Where Populate's tables land was not found (no warehouse/ dir appeared under /home/korp/.korp; rustfs/ exists). Next step is korp's: make Populate and IcebergTables agree on one catalogue, and say in the reply WHICH catalogue was written/read — a done that lists nothing afterwards is the same class of lie as korp-graph-9 was.

Saga tests (2 skipped) — the sink on :7878 is a 2026-07-14 nornir-server without RecordEvents (StatusCode.UNIMPLEMENTED). Rebuilding nornir-server from main on loki fails: registry znippy-zoomies 0.1.13 → registry znippy-compress 0.9.13 needs znippy_common::slotpool, which the sibling znippy-common (0.9.15) no longer has; a [patch.crates-io] to the sibling did not take (the requirement is pinned). That is nornir/znippy release drift, item for those repos; the skips stay honest.

Population — CLOSED, korp f6f83e5 (2026-09-19, later). The cause was found: Populate writes the FalkorDB graph and never a table; the tables come from korp demo into the embedded skade lake at <home>/.korp/demo_warehouse, and the server's Live warehouse binding resolved that path, named it in the reply, then read the Spark REST row instead. Now a bound path that is a directory is read as the local lake in all four arms. On loki: IcebergTables 91 rows, Population populated, test-demo 54 passed / 2 skipped (the saga sink). Item 6 is done except the saga sink, which is nornir's.