DOSSIER viewer — design
../software/programs/dossier.md— program charter (this opens its “browse” surface).../software/programs/dossier-data-model.md— the aspirational Org-tree intel model.- ADR-0040 — UDS / sanctioned-write boundary (this viewer is read-only).
- ADR-0042 — first-party on-device programs (DOSSIER is #11); enrich-never-gate.
- ADR-0046 — programs are knEmacs major-modes.
1. Problem
Section titled “1. Problem”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=routinghost:gateway-2:cred -> type=host entity=gateway-2 attr=credfile:/ops/plan.txt:exfiltrated -> type=file entity=/ops/plan.txt attr=exfiltratedGrouping (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.
3. Approved interaction model
Section titled “3. Approved interaction model”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
TYPEline + 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)”| Key | Action |
|---|---|
CAR | cursor up (previous entity) |
CDR | cursor down (next entity) |
INFO | inspect — toggle showing the full raw colon-path keys in the card |
BACK | exit the program |
(TERM reserved for the v2 completing-read find.)
4. The read seam (C)
Section titled “4. The read seam (C)”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.cis referenced by host-linked code, so it goes in bothhosts/emulator/CMakeLists.txtandhosts/device/CMakeLists.txtNOSH_SRCS(the hosts hand-copy the runtime source list — the known dual-list footgun).
5. The program (Lisp)
Section titled “5. The program (Lisp)”runtime/programs/dossier/dossier.lsp — a knEmacs major-mode, modeled on
kommander/ripsaw:
(dossier/enter)— definedossier-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/panelframes,ui/select-rowcursor,ui/peek-style card,ui/cliptruncation) + 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" }tok_program_libsinscreen_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).
6. Testing (TDD)
Section titled “6. Testing (TDD)”tests/test_dossier_screen.c— the read seam: bind into a bare fe context, register a seededDossierStore, assertcount/key-at/conf-at(in-range, oob → nil, NULL store → 0). Confidence-string round-trips.tests/test_dossier.c— the program through the router (mirrorstest_kommander.c): registration + launch-app routing +dossier-modedefined; 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) throughscreen_router_handle_key.
7. Demo
Section titled “7. Demo”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.
8. Out of scope (v2+)
Section titled “8. Out of scope (v2+)”- 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).