A video game where you play as a misaligned AI, deceiving and building power. An experiment in spec-driven development.
misaligned wiki interface context-menu.md
10 kB
Markdown
at commit e957ce7b

Spec: context menu — actions live on the thing #

Type: spec
Status: IMPLEMENTED
Status note: implemented 2026-07-07 on the context-menu worktree (all
  seven criteria). The read-only `Sim::available_actions(anchor) ->
  Vec<ActionDesc>` query lives in src/actions.rs as the single legality
  source, flattened to `MenuRow`s by `menu_rows` and dispatched back
  through existing commands by `Sim::execute_action`; no new sim behavior.
  The terminal and Bevy open the menu at the focused tile (Enter/`a`,
  right-click in Bevy); agent mode gains `actions [name|#flow]`.
  Anchor verbs were demoted off the per-anchor keys and rail/panel button
  rows in both frontends; only anchorless globals keep their keys
  (pause, speed, save, view flip — DESIGN.md "Actions live on the
  thing"). The allocation bar is a read-only fleet aggregate; mode
  changes live on the machine. 2026-07-08 follow-up: the leftover panel-open keys `r`/`e`/`t`/`u`
  and their modal panels are gone; research and drift-policy verbs live on
  the host rack, known-flow verbs hang on the switch, and earned off-map
  people hang on the host rack. Status (gap meter, known devices, slush,
  roster summaries) stays on the rail.
  2026-07-08 addendum implemented (event-to-anchor linking + the
  empty-menu feedback pulse, criteria A1-A5 below) on the event-anchor
  worktree: `LogEvent.anchor`, `Sim::anchor_position`,
  `Sim::menu_empty_feedback`; terminal `;` + `»` markers, Bevy clickable
  trace rows, agent `@anchor` suffixes + `focus last`.
  2026-07-08 epistemic-honesty follow-up: person verbs and
  signature-band observer strings in `ActionDesc` go through
  `Sim::person_label` / `Sim::observer_label` (cursor.md criterion 5) —
  role silhouettes until Schedule, authored names after.
Stage: B1 — The Basement
Constitution: "Actions live on the thing" (this spec's law),
  "Work is somewhere" (the anchors actions attach to),
  "Justification and legibility" (every entry shows cost and expected
  signature before commit; illegal entries show why),
  "Presence: the cursor and the senses" (the menu opens where attention
  is), "The terminal is a first-class frontend" (parity),
  "Automation as design language" (every automatable verb carries its
  automate affordance in the same menu)
Depends on: cursor.md (focus/inspection), reach.md (device anchors),
  day-job.md (attended work, the dial), economy.md (flow verbs),
  intel.md (tap/process/watch verbs), social.md (person verbs)

Behavior #

The focused anchor — a tile, device, machine, person, resident job, or known flow — exposes its legal verbs as a context menu opened at the point of focus. The menu is the primary action surface; the right rail is status and telemetry only.

  • One source of truth: the lib gains a read-only query, Sim::available_actions(anchor) -> Vec<ActionDesc>, where an ActionDesc carries: the verb, its Sim command, cost (compute / money / social pool), the expected signature (as the observer band it feeds, per the economy.md precedent), and — when the verb applies to the anchor type but is currently illegal — a disabled_reason string ("no egress channel", "not enough slush", "you can't see them"). Both frontends and agent mode render this list; no frontend duplicates legality logic.
  • Epistemic honesty: the query never returns a verb the player has not earned — unearned anchors expose nothing, and provenance rules (cursor.md) govern what the menu may name. Verb text that mentions a person, and the observer string on an expected-signature band, use Sim::person_label / Sim::observer_label — never an unearned authored name. Showing a disabled verb with its reason is for known possibilities; unknown ones are absent, not grayed.
  • Automation in place: any verb with a standing-policy form (attend -> standing dial, watch, scheme policies) shows its automate affordance as a submenu entry of the same verb, at its compute price.
  • Globals stay global: pause, speed, save/load, and view flip keep their keys; they act on no anchor. The fleet aggregate bar is read-only. Everything anchor-specific — including research tracks, drift policy, known-flow verbs, person verbs, and machine mode delegation — lives on the focused anchor's menu. There are no panel-open keys.

Player surface #

  • Terminal: cursor + Enter (or a) opens a compact overlay list at the cursor; j/k or numbers select; Enter executes; esc closes. Entries render verb cost [band] with disabled entries dimmed plus reason. The host rack's menu carries the day-job dial, research tracks, drift policy, and earned off-map people; a known device's tile (or the switch) carries digital and ledger/flow verbs; a visible person on a tile carries their social verbs.
  • Bevy: right-click (or Enter on the focused tile) opens the menu at the pointer; same content, same order, click to execute; esc/click-away closes. Bevy ASCII-folds the shared MenuRow::line separators (·/— -> |/-) because the embedded default font lacks those glyphs; selection is a solid amber wash with a leading > (terminal reverse-video parity). See wiki/interface/bevy.md.
  • Agent mode: an actions command prints the ActionDescs for the current focus in a stable line format; execution stays the existing verbs. Documented in agent-play.md.

Acceptance criteria #

  1. available_actions returns exactly the verbs executable on that anchor now, each with cost, expected signature band, and disabled_reason where applicable; unit tests cover a device, a person, the host rack's resident job, and a known flow.
  2. The query respects fog and provenance: an unearned anchor exposes no verbs, and no entry names a fact the player lacks (test: unseen person, untapped accounting system).
  3. Terminal renders the menu at the cursor with selection, execution, dim+reason for disabled entries; every previously key-bound anchor action is reachable through it.
  4. Bevy renders the same menu on right-click/Enter with identical content and order.
  5. Agent mode actions lists the same descriptors (stable format, documented); the act-one integration test still passes driven through existing verbs.
  6. No per-anchor action buttons remain on the sidebar/rail in either frontend; no panel-open keys (r/e/t/u); global keys are unchanged and listed in README + key hints (pause, speed, save/load, view flip). Fleet modes are delegated on the machine.
  7. Menu entries carrying an automate affordance show it in place with its compute price (day-job standing dial and one intel watch at minimum).

Addendum (2026-07-08): events carry you to the thing #

The follow-up the law implies: if verbs live on the anchor, the events about an anchor should carry your attention to it. Adopted with the 2026-07-08 DESIGN.md extension of "Actions live on the thing".

  • Anchored log events. Sim's log entry (LogEvent) gains an optional anchor: Option<Anchor> — an additive field, no log rework. The emitter sets it only where it knows the thing: job/core events → the host rack's tile; device verbs (tap/splice/take/compromise/egress splice) → the device; heard events → the person while sight covers them, else the room hearing earned; processed-intel results → the same person rule; ledger lines on known flows → the flow; scheme paydays and wager settlements → the switch when they ride the stolen egress (the sanctioned route hides in legitimate traffic and anchors nowhere); audit/filing events → nothing (they are panel facts with no honest map location).
  • Epistemic honesty. An anchor never encodes unearned knowledge: a heard-only person anchors to the room (its center tile), never to the person or their tile. Sim::anchor_position(anchor) is the one placement query — a person resolves only while sight covers them, a device at its blueprint-known tile, a flow nowhere (it lives on the ledger). Frontends never re-derive placement.
  • Focus links. Anchored lines are visibly marked and selectable: terminal — » marker; ; focuses the most recent anchored event (repeat presses walk older ones): cursor jumps to the anchor, its menu opens; a flow anchor opens its menu without a cursor move. Bevy — the RECENT TRACE rows are buttons; anchored rows carry a > marker and read bone-bright; clicking one moves the cursor (the camera follows) and opens the menu. Agent mode — drained event lines carry a stable suffix @tile(x,y) / @device(id) / @person(id) / @flow(id), and focus last jumps the cursor and prints the anchor's actions: lines (agent-play.md). An anchor the senses can no longer place is refused with a line, never silently.
  • The feedback pulse. Opening the menu on an anchor with zero rows answers instead of staying silent — silence read as broken input (repo-owner report, 2026-07-08). Sim::menu_empty_feedback(anchor) is the one source: a seen tile answers "No actions here."; an unearned (heard/remembered/blueprint/unknown) tile stays silent — answering would confirm input landed on world the senses have not placed; panel anchors (device/person/flow are known by construction) always answer.

Addendum acceptance criteria #

A1. LogEvent carries anchor; job, device, heard, ledger, and scheme events set it per the table above; audit events do not (unit tests cover a job event, a device event, and a ledger flow event). A2. A heard-only person event anchors to the room, never the person or their tile, and anchor_position(Person) is None while unseen (unit test). A3. Terminal: anchored lines carry »; ; walks anchored events newest-first, moving the cursor and opening the menu; unanchored lines are inert. A4. Bevy: trace rows are clickable; anchored rows are marked >; clicking focuses the anchor and opens its menu; unanchored rows do nothing. A5. Agent mode: event lines carry the stable @anchor suffix and focus last works, both documented in agent-play.md; the empty-menu pulse answers on seen tiles and stays silent on unearned tiles in all frontends (menu_empty_feedback unit test).