Skip to content

DOSSIER viewer — design


DOSSIER is a write target today (dossier-commit / dossier-has?) with no way for the operator to browse what’s been filed. :dossier is on the launch roster (screen_router.c k_programs) but registers no screen and has no lib, so launching it returns :unknown-app. Josh wants the player to open DOSSIER anytime and flip through filed intel “almost like a rolodex.” This builds that viewer and closes the stub.

2. The honest constraint (what’s actually in the store)

Section titled “2. The honest constraint (what’s actually in the store)”

The rich model in dossier-data-model.md (profiles, fact-nodes with values/provenance, a typed :links graph) is design-only — not built. The actual C store (dossier.c) is a flat fixed array of up to 64 facts, each just:

typedef struct { char key[64]; DossierConfidence conf; bool used; } DossierFact;

a colon-path key + a confidence (rumored < inferred < verified). No values, no links, no profiles. So the viewer derives its rolodex structure from the keys:

acct:fin-relay-07:routing -> type=acct entity=fin-relay-07 attr=routing
host:gateway-2:cred -> type=host entity=gateway-2 attr=cred
file:/ops/plan.txt:exfiltrated -> type=file entity=/ops/plan.txt attr=exfiltrated

Grouping (type, entity) yields a profile-like card whose “fields” are the attributes + their confidence. This stays cleanly read-only — nothing new writes (ADR-0040; durable consequence stays dossier-commit). Two consequences worth stating:

  • It’s the current run’s dossier, not a cross-mission archive yet. The store is run-state (reset at mission boundary; cross-mission persistence is still an open storage question in the docs). Bare-launch (no mission) → an empty rolodex, which matches the enrich-never-gate behavior; mid-mission → real filed intel.
  • No link-walking in v1. Links aren’t in the store. Navigation is type → entity → facts; graph-walking (person → the account it names) is a future hook for when the store gains links.

Index + card, two-pane (reuses the ripsaw/kommander 2-pane + ui/peek + tabulated-list family). Group by type; arrow-navigation only (search deferred to v2).

DOSSIER ⌁ 14 FACTS (Row 0: firmware)
┌ INTEL INDEX ───────────┐ ┌ acct:fin-relay-07 ──────────┐
│ ACCOUNTS │ │ TYPE account │
│ ▸ fin-relay-07 ▓▓ │ │ FACTS │
│ shell-3 ▓ │ │ routing ▓ verified│
│ HOSTS │ │ beneficial-owner ▒ inferred│
│ ▸ gateway-2 ▒ │ │ balance-est ░ rumored │
│ FILES │ │ │
│ ops/plan.txt ▓ │ │ 3 facts · 1 verified │
└────────────────────────┘ └─────────────────────────────┘
CAR/CDR flip INFO inspect BACK exit (Row 74: firmware action bar)
  • Left (index): type section headers (ACCOUNTS / HOSTS / FILES / PERSONS …) with the entities under each; each entity row shows a confidence cluster (one glyph per fact, or a summary glyph + count). The cursor lands on entity rows only (skips headers).
  • Right (card): the selected entity’s TYPE line + its facts (attr + confidence glyph/label) + a one-line summary. Updates live as the cursor moves — the “flip.”
  • Confidence glyph: fill-density maps to certainty — verified=▓ inferred=▒ rumored=░ (final glyphs pinned to whatever the KN-86 Code Page provides; chosen at implementation).
  • Empty state: count 0 → both panes show NO INTEL FILED; never raises.

Key surface (static dispatch alist — avoids the on-key cond GC hazard)

Section titled “Key surface (static dispatch alist — avoids the on-key cond GC hazard)”
KeyAction
CARcursor up (previous entity)
CDRcursor down (next entity)
INFOinspect — toggle showing the full raw colon-path keys in the card
BACKexit the program

(TERM reserved for the v2 completing-read find.)

A new runtime/src/dossier_screen.c — read-only enumeration over the live store, mirroring keystore_screen.c, bound router-side in router_program_context_ensure right after keystore_screen_bind so it is present in every per-program context and in the headless ctest router (the host bridge’s dossier-commit/has? are not present there — that’s why the seam binds router-side, not in the bridge).

(dossier/count) -> integer number of filed facts (0 if no store)
(dossier/key-at i) -> string|nil the colon-path key at slot i
(dossier/conf-at i) -> string|nil "verified" / "inferred" / "rumored"

Backed by a registered pointer to the live store (not a self-owned blob — the real facts live in SystemState.runtime.dossier):

void dossier_screen_bind(fe_Context *ctx); /* bind the 3 primitives */
void dossier_screen_set_store(const DossierStore *s); /* bridge sets &state->runtime.dossier */
void dossier_screen_reset(void); /* store := NULL (test isolation / empty) */

nosh_lisp_bridge_register calls dossier_screen_set_store(&state->runtime.dossier) so production wires the live store; a NULL store → count 0 (the safe bare default). Tests seed their own DossierStore and register it directly. No new write path.

CMake: dossier_screen.c is referenced by host-linked code, so it goes in both hosts/emulator/CMakeLists.txt and hosts/device/CMakeLists.txt NOSH_SRCS (the hosts hand-copy the runtime source list — the known dual-list footgun).

runtime/programs/dossier/dossier.lsp — a knEmacs major-mode, modeled on kommander/ripsaw:

  • (dossier/enter) — define dossier-mode, read the store via the FFI into a parsed model (list of (key . conf)), build the grouped display model, seat the cursor; never raises.
  • (dossier/render st) — paint the 2-pane (ui/panel frames, ui/select-row cursor, ui/peek-style card, ui/clip truncation) + the firmware chrome rows.
  • (dossier/on-key st k)static (key . thunk) alist dispatch (kommander pattern).
  • (deck/register-screen 'dossier dossier/render dossier/on-key).
  • Add { "dossier", "programs/dossier/dossier.lsp" } to k_program_libs in screen_router.c.

Derivation (pure Lisp): enumerate 0..count-1 → split each key on ":"type / entity / attr (type=seg0, entity=seg1, attr=join(seg2..); 2-seg keys → attr is the entity-level marker; 1-seg → type “misc”) → group by (type . entity), ordered by a canonical type order then entity name → flat display rows (headers + entities) for the index; the card is the cursor entity’s facts sorted confidence-desc then attr.

Public accessors for tests (headless, no render): (dossier/fact-count), (dossier/entity-count), (dossier/group-count), (dossier/cursor), (dossier/cursor-entity), (dossier/cursor-type), (dossier/cursor-down), (dossier/cursor-up), (dossier/card-text), (dossier/inspecting?), (dossier/toggle-inspect).

  1. tests/test_dossier_screen.c — the read seam: bind into a bare fe context, register a seeded DossierStore, assert count / key-at / conf-at (in-range, oob → nil, NULL store → 0). Confidence-string round-trips.
  2. tests/test_dossier.c — the program through the router (mirrors test_kommander.c): registration + launch-app routing + dossier-mode defined; render paints + leaves Row 0 / Row 74 untouched; the grouped model (counts, type order); cursor nav skips headers and flips the card; INFO toggles raw keys; bare launch (empty store) opens + renders + never raises; interactive key routing (CAR/CDR/BACK) through screen_router_handle_key.

A real-screen GIF via kn86rec --screen dossier (tools/record-demo.sh). The store is empty on a bare --screen launch, so the demo seeds a handful of representative facts first (a demo-harness concern — likely a --world-style seed or a small .rec directive that registers a seeded store), then flips through the rolodex. The empty state is real product behavior; the seed exists only to populate the GIF.

  • completing-read find / filter (TERM).
  • Link-walking / cross-entity jumps (needs the store to carry links).
  • Cross-mission durable aggregation (needs the storage-home decision from the data-model doc).
  • Operator-notes editing lane (the data-model’s one writable field).