← Design · c131

synthetic-data.md

c131: Synthetic data for the native app's pages

Reader: Mark; the engineer who owns the plane's data model; c131's executor; c132 (the same world feeds the mocks). Written: 2026-11-10, an outline to rule on, not a spec; ruled 2026-11-11 (§Rulings: five rulings, walked twice with Mark). Rests on: c131 R1–R3 and D3–D5, c130 D6 (three sources), qdash's data-interface bible R23–R25, the Meridian seed (../qcontrol/scripts/qdash-seed/), c113's baked worlds, the c1 spike's fixtures crate.

The principle

  • One world, one timeline, one cast. The web demo already has it: Meridian, a hand-authored organisation that the seed expands into the same run and scan streams the real sensor emits, replayed through qdash's real ingest path, deterministic (seed 42, day 90 = 2026-07-01). The native app's synthetic data is not a second world; it is Meridian cut to one machine.
  • Synthetic flows through the real pipes (R1). The fixture is served JSON at the real routes, read through the same repositories and the same nozzle as live data. The footer says data: synthetic · Meridian · day 90. No page holds a fake.
  • Two cuts of the same world. The fleet cut is what the control plane shows (every endpoint, every user). The one-machine cut is what the native app shows: one endpoint, its user, what qcontrol found there, plus what only the endpoint knows (the sensor's state, the config file, resources, logs). The second is a filter of the first plus a small set of endpoint-only shapes.
  • Three sources, one world. synthetic reads the bundled cut; local reads this machine's qcontrol; plane reads a plane that serves the same world, the fixture plane today and the real plane later. The ask to engineering follows from this: the new data model should be able to emit Meridian, so web and native agree by construction.

What the world already has (Meridian, world.json)

PartContentServes
orgMeridian, a domain, an org id, a qcontrol version, default pluginsStatus (version, plugins), Connect (org)
teams · usersPlatform, Payments, Data, Frontend; ten people (priya, dana, raj, marcus, tomas, sofia, wei, elena, jake, amara)the one-machine cut picks one user's laptop
agents · installsclaude-cli, claude-desktop, codex-cli, cursor-cli, gemini-cli, windsurf, ollama; rollout day, version drift, shadow installsInventory: harnesses, installations
mcp_servers · builtin_tools · skillsfive MCP servers with their tools, eight built-in tools, a skills catalog with per-team MCP, per-agent plugins and driftInventory: MCP servers, tools, skills
prompts · volumetwelve prompts; target runs, cost, responses and tool calls per run, file events, weekend factorEvents (the stream), Status (activity counts), sessions
arcpilot users, the paste scare, env noise, the Gemini shadow then sanctioned day, an incident and its response, fresh egress, a secret read, a quota overageEvents: the interesting days; findings and decisions on this machine
stateegress decisions, the finding workflow, controls, attestations, channels, campaignsConfig: which policies apply here; the audit log

Served today by qdash's 38 routes with one TypeScript type per shape in ui/app/types/api/* (harness, mcpServer, skill, tool, session, workspace, model, provider, endpoint, installation, live, stats, performance, finding, control, …). c113 bakes the world into files per scenario (company sizes, First run stages, exception scenarios) and serves them from R2, switchable instantly. qnative c2 (2026-11-10) replaced the spike's hand-written stand-in with real cuts: scripts/cut-world.py expands the seed in memory, keeps one host's records, ingests them through the real qdash binary and crawls every Inventory route; three worlds are bundled (meridian-day90 = priya-x1, minimal = chen-mba, fresh = empty; 134, 96 and 44 responses), each checked against the generated types by a strict drift test.

Per page: what the one-machine cut must carry

PageServed shapes (exist)The one-machine cutEndpoint-only shapes (new)States to story
StatusStats (/api/stats), Performance and PerformanceAgent (/api/performance) filtered to this hostactivity counts and per-agent performance for one endpointSensorState (off · starting · on · degraded; version; uptime; last seen by the plane), Resources (CPU, RSS, workers), EffectiveConfig summary (file found, mode, plane connection), LogLine tailoff, starting, on-local, on-connected, degraded
InventoryHarness, McpServer, Skill, Tool, Session + Workspace, Model + Provider, Installationthe fleet rows filtered to one host: installs and hosts collapse to 0 or 1, versions to the one installed, sessions to this user'snone; the shapes holdempty (fresh install, day 0), minimal (one agent, week one), typical (day 90), overflow (hundreds of sessions), error
EventsStreamEvent (/api/events/stream), the activity and audit rows (/api/activity, SessionEvent)the stream replayed for one host from the seed's runs.ndjson and scan.ndjson; the arc's days are the scenariosnoneidle, streaming, paused, filtered to nothing, error
ConfigControl and the policy state as they apply herewhich policies and controls reach this endpoint, their mode and versionConfigFile (path, raw text, parsed, valid or the error), EffectiveConfig (defaults · file · plane, which won)local editable, connected read-only, invalid file
Connectnone served today—PlaneConnection (base URL, org, endpoint id, state, credential expiry, last sync)not connected, signing in, connected, failed (the status code, honestly)
Messages (c137)none served today—Message per the bones; the ten example messages in c137/bones.md are the fixturenew, seen, acknowledged, done, expired; the withheld count
Grant line (c136)none served today—a grant summary (purpose, level, until) or nothingnone, active

The endpoint-only shapes are the real gap. They come from qcontrol's own crates (qconfig, qevents, the supervisor), not from qdash's handlers, so R3's "written from the backend handler" needs its second source: a decision for Tyler (c130 Q3) and the plane's owner. Recommendation in the open question below: new qdash routes, typed and generated like the rest.

The scenarios: named worlds, the same names as the web demo

One world, several cuts and days, each a directory of JSON files named by route plus a small manifest (world.json: name, anchor day, host, user, sensor state, plane state). The native app selects one with --world <name> or QPOINT_WORLD, and from its own settings while the source is synthetic (§Rulings, 3); the lab and the stories select the same by name, so a capture of the web mock and a capture of the native page show the same rows.

WorldWhat it isPage it exists for
freshday 0: qcontrol just installed, nothing found yet, no plane (bundled)Inventory empty, Status on-local, Connect not connected
minimalchen-mba at day 90: the day-89 new hire, one instrumented agent, a handful of sessions, an unmonitored Windsurf install (bundled)Inventory minimal
meridian-day90 (default)nina-mbp at day 90 (ruled 2026-11-11): a quiet laptop, Frontend, three agents since day 36, nothing in the arc; to be recut from priya-x1every page, typical
pilotpriya-x1 at day 90: the pilot user and migration lead, the 3× spend outlier, incident-response sessions with approvals bypassed; the world the violation tour switches to (bundled today under the default's name; to rename)every page with findings, Messages, the tour
power-usera user with hundreds of sessions and workspacesInventory and Events overflow
incidentthe arc's incident day: the paste scare, the secret read, the egress decisions, a finding and its noticeEvents streaming, Messages (an enforcement notice), Config (a policy changed)
connectedmeridian-day90 with the plane connected: Config read-only, the identity confirmation in the inboxStatus on-connected, Config read-only, Connect connected
broken-configthe file found but invalidConfig invalid, Status degraded
plane-downconnected, then the plane stops answeringStatus degraded, Connect failed, Events error

The web demo's worlds (c113: sizes, First run stages, scenarios) and these share the Meridian cast and timeline; a world the demo bakes for the fleet is the same day the native app cuts for one host.

Where it lives and how it is produced

  1. Source of truth: ../qcontrol/scripts/qdash-seed/world.json and generate.py. The export step exists in qnative (c2): scripts/cut-world.py --host <host> --name <world> imports generate.py read-only, expands the streams in memory, keeps one host's records, ingests them through the real qdash binary and crawls every route the pages read into one file, { manifest, responses } keyed by route and sorted query. Deterministic like the rest. Moving it beside the seed in qcontrol is the ask below.
  2. Bundled: three worlds in ../qnative/crates/fixtures/world/ through include_str! (BUNDLED: meridian-day90, minimal, fresh; under a megabyte together, parsed once); others loadable from the standard path (c131 D4: $XDG_DATA_HOME/qpoint/synthetic/<world>/ or the platform equivalent).
  3. The plane: the fixture plane serves the same directory at the same routes with an optional credential (built in the spike); the real plane serves the export of its own data model.
  4. The lab (design): the same JSON feeds the Vue mocks in a native skin (c132 medium A) through one composable, useNativeWorld(name), so a lab page and a native story render the same rows and the captures line up.
  5. Validation: exists: every_response_matches_the_contract in fixtures deserialises every response of every world into the generated types, strict about unnamed fields with --features types/strict; each story names its world, so the state ladder enumerates the worlds and nothing is drawn from a world that does not exist.
  6. Time: the world is anchored on 2026-07-01 and the app shows "now". Shift every timestamp at load by a fixed offset so the world's "today" is the launch day (what c113's bake does when it rolls a world forward), and say so in the footer.

What this asks of the plane's data model

  • An export of Meridian from the new model, by host and by day, in the served shapes: the seed already "doubles as the contract the future cloud backend must satisfy" (its README); the native cut makes that concrete.
  • The event stream as the same records the seed replays (runs.ndjson, scan.ndjson), so a streamed event and a stored one are one shape.
  • The endpoint-only shapes agreed once, between qcontrol (which produces them) and the plane (which stores the ones it should see).

Rulings (2026-11-11)

Mark's answers to the four questions, in the order asked.

  1. The default is a quiet laptop. The default world shows a person doing nothing risky. The cut is nina-mbp, confirmed in the second walk over omar-mba and a new cast member: Frontend; claude-cli, cursor-cli and Claude Desktop since day 36; no personal account, no shadow install, nothing in the arc. meridian-day90 is recut to nina and the day-90 cut of priya-x1 is kept as its own world, pilot.
  2. Priya's laptop is the world you switch to when you want to see what a violation looks like, and that is a tour. Mark's three steps: switch to the synthetic data (the pilot world); simulate a bad event; watch the notices fire locally and tune how much gets through. In c137's vocabulary the alarm is an enforcement notice from qcontrol in local mode (R4: nothing is alarming; attention and required, never danger), and the tuning is the citizen's controls (per type, quiet hours, mute). What it needs from the app: the world switch (3); a drill action on the synthetic source that injects an arc event into this machine's stream (the web tours' Simulate violation; the native tour runtime keeps the action's place but has no counterpart yet, qnative tour/data.rs); c137's bones and controls wired (its Phase 3). The simulate step injects any of three arc events (second walk): the paste scare (day 38, code sent to pastebin.com: an egress notice), the secret-file read (day 88, config/secrets.yaml opened three times: a sensitive-file notice) and the incident (day 61, AWS credentials opened seven times and egress to an unknown host: the strongest notice). If three drills are too much for one tour, they are three violation tours, one per notice (Mark). It is the first native tour. It lives in c131 with c137 and the Tour stretch; c0 names it.
  3. The world is chosen in the app's settings (Mark's preference), shown only while the source is synthetic; the flag and QPOINT_WORLD stay for captures. Considerations, from the code as it stands (main.rs, nozzle/repo.rs, router.rs, pages/mod.rs): the switch is read once at startup and the Repo (an Arc<dyn DataNozzle>) sits in a GPUI global, RepoHandle; pages fetch through router::repo(cx) and pages::load, which runs the read on the background executor and sets a Load<T> on the page entity; nothing else caches rows. So a swap is: replace the global with a new Repo over the other world, bump a generation counter, and let every page observe the global (observe_global) and run its load again; the footer re-reads describe(), the tray its line, the tour its facts. Three things to carry: a read started on the old world that finishes after the swap is dropped (the generation check in load's setter); per-world UI state (c137's seen and acknowledged, mutes) is reset for synthetic worlds and kept per source for local and plane; the bundled worlds are parsed together into one OnceLock map (a few megabytes resident; parse per name if the budget minds). No GPUI re-initialisation is needed. The same re-open path is what Connect needs to move the process from local to plane without a relaunch (c131 D13), so building it for worlds builds it for Connect.
  4. The lab adopts the same worlds in c132 Phase 0. One composable, useNativeWorld(name), reads ../qnative/crates/fixtures/world/<name>.json by the same request key the nozzle uses; the native-skin mocks stop inventing rows.
  5. The six endpoint-only shapes become qdash routes, typed and generated like the other 38 (second walk; detail below). Not hand-written in types, not imported from qcontrol's internal structs.

Recorded as c131 D15–D17, c132 D6, a paragraph under c137's Phase 3 and its question 5. The recut, the rename and the settings switch are handed to qnative c2 as a hand-on file, ../qnative/cycles/c2.expressive-range/handon-2026-11-11.md (the cross-session message was held undelivered); nothing else in ../qnative changed from design.

The six endpoint-only shapes (ruling 5, in detail)

SensorState, Resources, EffectiveConfig, LogLine, ConfigFile, PlaneConnection. The 38 served shapes have one mechanism today: typed in qdash's TypeScript (ui/app/types/api/*), generated into Rust (types/api.rs, qnative c2), checked by the strict drift test. The six new shapes had no handler, so nothing typed them. Ruled 2026-11-11: they get the same mechanism. New qdash routes (/api/sensor, /api/config, /api/plane), typed in TypeScript beside the rest, generated into Rust like the rest, under the same test; qdash runs inside qcontrol and reads qconfig and the supervisor in process, so the backend handler R3 asks for exists once the route does. One mechanism for all 44 shapes, and the local source reads the six over the same loopback API it reads everything else from.

Not chosen: hand-writing the six in types from recorded output (the contract in two places until a check is added; acceptable only as a bridge if the routes cannot land before Status is built), and importing qcontrol's internal structs into the native app (those are the engine, not the protocol). The cost is engineering time in qcontrol: the ask joins the plane engineer's list beside the "one contract" item (c134 kamal-prep.md), and the six shapes are the first entries of c134's API document.

Developer mode's Monitor reads two of these routes (2026-11-12, c143). c138's Monitor, the replica of the monitor TUI, needs more of the same facts: the monitor's pid, a row per process in its tree with 1 Hz samples, health and the last error, the log's target and a sequence number to follow it, and the targets and behaviors as the monitor has loaded them. They are optional fields on /api/sensor and /api/config rather than shapes of their own, so the ask stays one per route; SensorState.state and degraded are the rollup of the new health. V1 reads none of them. Typed in the bridge (app/data/worlds/bridge/types.ts); the Monitor's two routes of its own are in qnative's Guide page 4.

Qpoint Brand Style Guide