Skip to content

Experiment rust repl + tui#100

Open
Ignition wants to merge 142 commits into
masterfrom
feat/rust
Open

Experiment rust repl + tui#100
Ignition wants to merge 142 commits into
masterfrom
feat/rust

Conversation

@Ignition

Copy link
Copy Markdown
Contributor

No description provided.

Ignition added 30 commits June 10, 2026 00:19
Capture the design from the grilling session for a Rust Memgraph console:

- CONTEXT.md glossary (Core, Frontend, Session, Value, Record, Import mode,
  cypherl, vertices-first ordering)
- ADR 0001: pure-Rust Bolt (bolt-client/bolt-proto) over mgclient FFI
- ADR 0002: async Core with a Core/Frontend seam, streaming-first records
- PRD with 45 user stories and testing via live memgraph/memgraph containers
- 32 tracer-bullet issues (dependency-ordered, TDD-sized) + README landing page
Throwaway testcontainers spike (memgraph:3.10.1, Bolt v4.4): all 21
Memgraph Value types decode faithfully through bolt-client 0.11 +
bolt-proto 0.12, zero codec extensions. ADR 0001 confirmed; mgclient
FFI fallback not triggered.

Grilling outcomes folded in:
- ADR 0003: Core-owned Value model, enum normalised at the Bolt boundary
- ADR 0004: owned RecordStream + QueryResult shape (summary after drain)
- CONTEXT: add Enum, Query result; rename Result stream -> Record stream
- Sharpen issues 02/06/07/13/14/32 (incl. TransientError is not a
  reliable retryable signal)
- Local testing only, no CI for now

🤖 Generated with [Claude Code](https://claude.com/claude-code)
- Two-crate workspace: mgconsole-core (lib) + mgconsole (binary).
- Session connects by host/port over pure-Rust Bolt (v4.4..v4.1), runs a
  query, returns QueryResult { header, RecordStream, summary } (ADR 0004).
- bolt_proto::Value -> core Value translated at the boundary; Value is
  total (every spike-confirmed type) and the enum sentinel map is
  normalised to Value::Enum here (ADR 0003).
- RecordStream is owned (no futures::Stream in the public API); next/collect
  are async so slice 21 lazy-pull is a drop-in.
- CLI pipes a query from stdin, drives the async core via block_on, prints
  tab-separated tabular cells.
- testcontainers harness (tests/common) + RETURN 1 integration test; local
  cargo test, Docker daemon only. No OpenSSL/C linkage.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
- Float rendering keeps a decimal point (3.0 not 3); NaN/Inf handled.
- String rendering escapes control whitespace (\\ \n \r \t) so cells stay
  on one line; quotes left literal (CSV quoting is slice 22).
- Reusable golden harness (tests/golden/check_tabular) comparing rendered
  cases to tests/golden/tabular/<category>.txt; UPDATE_GOLDEN=1 regenerates.
  Slices 04-07 add categories.
- Tabular conventions documented in the issue (mgconsole goldens absent here).

🤖 Generated with [Claude Code](https://claude.com/claude-code)
- Thread a `quote` flag through rendering: a top-level string cell is
  unquoted, but strings nested in a list/map are quoted with full escaping
  (mgconsole/Cypher convention).
- Lists/maps render with elements/pairs, empty forms, arbitrary nesting;
  map keys unquoted and sorted (BTreeMap) for deterministic goldens.
- New `containers` golden category.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
- Node -> (:Label1:Label2 {props}); collapses cleanly with no labels/props.
- Relationship/UnboundRelationship -> [:TYPE {props}] (standalone form,
  no endpoints).
- Path reconstructed as (n)-[r]->(n)... from Bolt's node/rel/sequence,
  honouring per-hop direction (signed relationship index).
- Property values reuse the nested (quoted-string) rendering from 03-04.
- New `graph` golden category.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
Match Memgraph's textual conventions (captured via toString against a live
3.10.1 container):
- 6-digit microsecond precision on all time-bearing types.
- date YYYY-MM-DD; localtime/localdatetime ISO with T separator.
- duration P{days}DT{h}H{m}M{s}.{micros}S (months folded in only if present).
- datetime offset ...+02:00; named-zone shows both offset and [Zone].
- Both zoned arms handled: DateTimeOffset and DateTimeZoned.
- New `temporal` golden category.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
- Point2d/Point3d render with SRID and coordinates:
  point({srid: 7203, x: 1.0, y: 2.0}) etc. (Memgraph toString does not
  support points, so this is a defined convention).
- Enum already normalised to Value::Enum at the boundary (ADR 0003);
  renders as Type::Member (Status::Active).
- New `point_enum` golden category. Rendering seam (03-07) now complete:
  every Value arm is real, no provisional/Debug renderings remain.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
- core::tabular::render_table via comfy-table (ASCII_FULL, force_no_tty for
  deterministic goldens): header + content-sized aligned columns.
- TableOptions.fit_width fits the table to a terminal width (cells wrap).
- RecordStream::collect_capped(cap) -> (rows, overflowed): never holds more
  than `cap` rows (one extra peeked to detect overflow), so the buffered
  path is bounded (ADR 0002). row_cap_warning points to streaming formats.
- CLI renders a real table and warns on overflow.
- Block-golden harness (check_block) + `table` goldens: basic, wide, fit.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
- QueryAssembler.push(chunk) -> completed queries; unterminated tail retained
  and re-scanned on the next feed (so strings/comments spanning feeds work).
- Char scanner ignores `;` inside '..'/".."/`..` and // and /* */; honours
  backslash escapes and doubled-backtick escapes; skips empty queries.
- 10 pure unit tests covering each case.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
- scan_clauses(query) -> BTreeSet<Clause> over a string/comment-skipping,
  case-folding word tokenizer: Create, Match, Merge, IndexCreate, IndexDrop,
  DetachDelete, Delete, Remove, StorageMode.
- CREATE INDEX != plain CREATE; DETACH DELETE != plain DELETE.
- Keywords inside literals/comments ignored; word boundaries stop `created`
  matching CREATE; composes across a multiline query.
- 8 pure unit tests. Used by parser mode (28) and vertices-first (31).

🤖 Generated with [Claude Code](https://claude.com/claude-code)
- RecordStream now pulls lazily in batches (DEFAULT_BATCH_SIZE=1000) from a
  connection shared with the Session via Arc<Mutex<Conn>>; never holds more
  than one batch, so memory is bounded on arbitrarily large results.
- Session.run no longer eager-pulls; the signature (QueryResult{header,
  RecordStream,summary}) is unchanged — no downstream migration (ADR 0004).
- RecordStream::discard() drains the server-side stream (Bolt DISCARD) so the
  Session is reusable after a partial/capped read; CLI discards post-cap.
- Buffered variant retained for unit-testing the cap/collect logic.
- Shared Bolt metadata helpers in proto.rs (fields/failure/has_more).
- Integration tests: stream 50k rows incrementally; discard-then-reuse.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
Row-oriented RowWriter trait + write_stream driver that streams a RecordStream
one Record at a time (bounded memory, ADR 0002).

- 22 csv: csv crate; configurable delimiter/quote/escape/double_quote; null is
  an empty cell, composites reuse tabular rendering; crate handles quoting of
  cells with delimiters/quotes/newlines.
- 23 jsonl: one JSON object per row keyed by column; documented JSON encoding
  per Value type (graph/temporal/spatial/enum); valid JSONL.
- 24 cypherl: replayable statements one per line (DUMP-style), exactly one
  trailing semicolon (idempotent); round-trip validated later by slice 26.

Error gains an Output variant + From<io::Error>.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
Decisions from a plan stress-test (no production code change):

- ADR 0005: Session guards "one live result at a time" at runtime (keeps the
  Arc<Mutex<Conn>> cancellation seam) rather than borrow-enforcing it; folded
  into issue 14 (ResultStillOpen + RESET-on-abandoned).
- ADR 0006: parallel import uses worker-pull (N worker Sessions on a shared
  Batch queue), NOT a connection-pool crate — drops the deadpool-vs-bb8
  question. Re-scoped issues 29/30; updated PRD + README.
- Line editor: rustyline over reedline (frontend-local, maps to Validator/
  Highlighter/Completer/FileBackedHistory). Updated issues 16/18.
- Integration-test strategy: shared container per test binary + reset(),
  recorded in the test harness for slices 12-14+ to adopt.
- Floats/null deliberately diverge from Memgraph toString (faithful type
  display, round-trip) — rationale documented in issue 03.
- Dev hygiene: rust-toolchain.toml (clippy/rustfmt components), workspace
  lints forbidding unsafe (ADR 0001 now compiler-enforced), cargo `lint` alias.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
Folds the integration-test-strategy decision into the first slice that adds
integration tests, rather than spawning a separate issue.
Session::connect_with carries optional Credentials over scheme:basic and maps a
HELLO refusal to Error::Auth. cli resolve_password injects the no-echo prompt so
it's pure-tested; main wires rpassword. Integration test authenticates against a
credentialed container and asserts wrong creds yield Error::Auth.
ADR 0007 records the rustls+ring backend choice (no OpenSSL; relaxes ADR 0001's
no-C-toolchain line) and the no-verify policy mirroring mgclient REQUIRE. A
MaybeTlsStream enum keeps the Client monomorphic; connect params consolidate into
ConnectOptions { credentials, use_tls }. Integration test generates a self-signed
cert with rcgen, starts an SSL Memgraph, asserts TLS connects and plaintext to
the TLS port fails.
Session::run_with_params binds named params; value::to_bolt encodes Core Values
to Bolt, rejecting structural types (node/rel/path/enum) as Error::Parameter.
Introduces the one-container-per-binary lease() harness with reset() and
converts the session tests to it; auth/TLS keep dedicated containers. Pure
encode tests + integration tests for mixed-kind params and a stored-data lookup.
Summary now carries notifications, stats, and residual trailing metadata; it is
built from the RUN reply and completed when the record stream drains/discards
(absorb_terminal), so QueryResult.summary() reflects post-drain data per ADR
0004. ExecutionInfo lifts Memgraph's cost_estimate/parsing/planning/
plan_execution times. Keys discovered from a live container probe; integration
tests assert stats, notifications, exec info, and clean absences.
QueryError carries the full/leaf code; is_retryable() keys off the leaf, not the
TransientError tier (spike: a permanent error wore a TransientError code). RUN
FAILURE now RESETs so the Session survives a query error. Fatal transport errors
reconnect with bounded retries (stored ConnectOptions) and retry once; exhaustion
is a terminal Error::Connection. ADR 0005 guard: ResultGuard liveness token makes
run() return ResultStillOpen for a live result and RESET-recover an abandoned one.
Adds a severable TCP proxy test harness; integration tests cover all five paths.
Ignition added 30 commits June 11, 2026 07:59
Give each Buffer tab an identity and make the bar scale. A tab is
auto-titled from a short snippet of its content — the originating query
of its shown Result-history entry, falling back to the first non-empty
editor line, then the Buffer number — truncated to a small width, the
active tab highlighted, the Buffer owning the in-flight query marked •.

When the tabs exceed the bar width it windows to keep the active tab
visible, with ‹/› overflow markers. Titling, windowing, and the click
hit-test share one pure layout (WorkbenchState::layout_tabs), cached on
the state as tab_hits, so a tab-bar click maps to the right Buffer under
windowing and variable title widths. Replaces the old fixed-width
numbered tabs and the integer-division hit-test.
Extend the theme beyond highlight categories to the Workbench chrome:

- Palette gains border / selection / status slots, overridable through
  the existing [theme] table and carried by every built-in. The draw
  consults them for pane/overlay borders, the selected cell/row, and the
  status bar — no remaining hardcoded Cyan/reverse for these. An absent
  override reproduces today's appearance (Cyan focused borders,
  reverse-video selection, unstyled status — the Default sentinel).
- A new light built-in joins default/mono, tuned for light-background
  terminals (darker base ANSI colours, legible comment grey, blue
  chrome). :set theme light selects it and it is listed among the
  built-ins.

selection_style maps Default→reverse-video and a set colour→background,
so themes can recolour the selection without losing the no-colour cue.
The operations that replace the whole editor buffer — auto-format,
:load, and history recall — used to rebuild the text-area widget,
discarding its undo stack, so a format could not be reverted and
recalling over unsaved text lost it irrecoverably.

EditorState now keeps a snapshot undo/redo stack: each logical edit (a
keystroke, newline, completion, or whole-buffer replace) checkpoints the
prior buffer, so a single Ctrl+Z restores it regardless of how many
internal text-area edits it made (a replace is otherwise two). Ctrl+Y
redoes. Snapshots are cheap for query text and sidestep tui-textarea's
per-internal-edit history.

Ctrl+Z/Ctrl+Y are editor-pane chords; from the results pane Ctrl+Y still
reaches the summary-drawer gesture, so nothing is lost to the collision.
Make the Workbench's gestures discoverable:

- Bare ? opens the :help overlay when the editor is empty or the results
  pane is focused; typing ? into a non-empty query still inserts it, so a
  ? in Cypher text is never swallowed.
- The status-bar hint is regenerated from the live [keys] (status_hint)
  so it shows the user's actual chords — e.g. a rebound format key —
  rather than a stale fixed string, and carries a '? help' pointer.
- The help overlay already reads the live [keys]; a test now pins that it
  reflects a rebinding and lists the remapped nav chords and :close.
The :help and cell-detail overlays are scrollable but gave no cue when
there was more content off-screen. They now show a ▲ near the top-right
when there is content above the view and a ▼ near the bottom-right when
there is more below, so it is clear when — and which way — to scroll.
Nothing is drawn when the content fits.

A wrapped_line_count helper estimates the wrapped height (ratatui's
public line_count is feature-gated), driving the indicator. Pure
presentation; covered by draw tests for the overflow and fits cases.
Two refinements from a grilling session against the domain model.

:close is a Workbench command, not a shared meta-command. CONTEXT.md
defines a Workbench command as having no meaning in the line REPL (which
reports it unknown), but issue 02 had made it a recognised MetaCommand
the REPL reported as 'unavailable'. Removed MetaCommand::Close; the
Workbench now parses :close in its own command layer (WorkbenchCommand)
ahead of the shared parser, and the REPL reports it as a plain unknown
command. :close is also no longer refused mid-query: it always closes
the active buffer, cancelling the in-flight query when that buffer owns
it (close_buffer fixes up the running-query index across the removal).

Ctrl+Y is the editor's redo, not the summary toggle. The editor owns the
Ctrl/Alt+letter chords it uses for editing (ADR 0016, narrow reading of
the PRD ruling); redo is Ctrl+Y for terminal portability, so the summary
drawer moved to Ctrl+N. Undo/redo now bind globally (no focus-scoping)
since the collision is gone.

Adds ADR 0016 recording the keybinding-ownership boundary.
Plan for round 2: modal Command line (editor is Cypher-only), Tab/Shift+Tab
keyspace, Transaction episode model + correlated history, bolder tx indicator,
and a clipboard helper process with OSC 52 fallback. New CONTEXT terms
(Command line, Transaction episode); ADR 0017 (modal command line) and ADR 0018
(clipboard helper over OSC 52, refining 0015).
The Buffer editor was a dual-purpose surface routing :-lines to the meta
parser; that forced Tab to mean two things and made : un-typeable for labels
in non-empty buffers. ADR 0017: a modal Command line owns the typed
:-vocabulary, the editor holds only Cypher.

- New CommandLine state + draw over the status row, owning the cursor while open.
- Opens on : while the editor is empty/whitespace-only, or on Ctrl+G anywhere
  (new rebindable Gesture::OpenCommandLine, default ctrl+g per ADR 0017).
  Enter runs via the existing Workbench-command / MetaCommand path; Esc cancels;
  focus returns to where it was. Not part of the focus cycle.
- Editor submit no longer routes :-lines to meta — a :-line is Cypher and errors.
- handle_meta no longer clears the editor (the command line is a separate
  surface), so a half-written query survives a mid-composition command. :load
  still recalls into the editor; :close no longer wipes the last buffer.
- Migrated all :-command reducer tests onto the command line; new tests cover
  open-on-empty vs literal-:, the Ctrl+G opener, run, cancel, and
  editor-submit-is-Cypher. 207 workbench tests green; lean build + clippy clean.
Make the modal Command line ergonomic (issue 02):

- Tab completes a :command name from the existing completion engine — a new
  CommandVocabulary source over COMMAND_NAMES, reached via
  Completer::with_command_vocabulary(). A repeated Tab cycles the matching
  candidates inline; with no candidates it is a no-op (never switches focus).
- Up/Down recall past :-commands from a new session-scoped command_history
  stack, distinct from the Cypher query history that Ctrl+Up/Down recalls into
  the editor. Stepping past the newest restores the in-progress line.
- CommandLine carries the inline completion menu (CommandCompletion) and the
  recall position; an edit drops the menu.

Tests cover command-name completion, cycling, no-candidate no-op, recall in
both directions across multiple entries, and the two histories staying
distinct. 214 workbench tests green; clippy clean.
…witch

Removes Tab's overload in the editor (ADR 0017): Tab no longer cycles pane
focus when there is nothing to complete.

- Editor Tab is completion-only — opens the popup, or is a no-op with no
  candidates; it never switches focus.
- New rebindable Gesture::SwitchFocus (default Shift+Tab) toggles editor↔results
  focus when the completion popup is closed; with the popup open, Shift+Tab is
  caught first as prev-candidate (the mirror of Tab's next).
- Results-pane Tab is now a no-op (focus-switch is Shift+Tab).
- IO edge normalises crossterm BackTab → Tab + shift (shift forced, since some
  terminals omit the modifier on BackTab).
- Status hint and help reflect Tab: complete / Shift+Tab: focus.

Tests: Shift+Tab focus-switch both directions, plain Tab no-op on empty editor,
Tab-still-opens-completion, Shift+Tab prev-candidate with wrap. 216 workbench
tests green; clippy + lean build clean.
…tory

A query run inside :begin…:commit/:rollback was a bare success whose rows might
since have been rolled away. Introduce the Transaction episode (CONTEXT.md): a
Session-scoped, numbered :begin→end lifetime with per-statement ordinals and a
final disposition.

- Session-scoped tx_episode / tx_ordinal counters; each submission made while a
  transaction is open is tagged with {episode, ordinal, Open}. Counters are
  global, so ordinals continue across Buffers within one episode.
- :commit/:rollback resolve the episode retroactively across every Buffer's
  Result history (open → committed / rolled back), driven by the actual
  TransactionApplied outcome via a remembered pending op — so a poisoned :commit
  the server rejects does not resolve it; only :rollback does.
- A rolled-back entry keeps its rows but is marked undone. A :connect swap
  resolves the aborted episode as rolled back. Autocommit entries are untagged.
- Draw renders 'tx N · stmt k [disposition]' in the results-pane header
  (extracted results_title helper).

Tests: ordinals across Buffers, retroactive commit resolution across Buffers,
rolled-back-keeps-rows, poisoned→rolled-back, second-episode numbering, plus
draw tests for the header tag and autocommit-untagged. 224 workbench tests
green; clippy + lean build clean.
The dim [tx] / [tx failed] marker was too easy to miss. Replace it with a loud,
glanceable status segment.

- New theme UI slots transaction / transaction_failed (extending issue 08),
  carried by every built-in: amber/red on default, monochrome on mono, blue/red
  on light (a Yellow block washes out on white). Both overridable via [theme].
- The status bar draws TX N OPEN (open) or TX N FAILED (poisoned) reverse-video
  + bold so the slot colour reads as a solid block; N is the Transaction episode
  number (issue 04). Autocommit shows no segment.
- status_text no longer carries the dim [tx]; draw_status prepends the segment.

Tests: draw segment for open/failed/autocommit, the slot colours resolve over
the active theme incl. light, and the theme slots resolve + override. 226
workbench tests green; clippy + lean build clean.
ADR 0015's accepted cost — yank silently no-ops on a terminal that ignores OSC
52 — turned out to be the common *local* case (a Wayland VTE terminal). ADR 0018
refines the mechanism ordering: prefer a local helper process, fall back to OSC
52. No FFI, no new crate (ADR 0001 stands) — just std::process::Command.

- New workbench::clipboard module: probe wl-copy → xclip → xsel → pbcopy →
  clip.exe on PATH, pipe the rendered text to the first present; a helper that
  exists but fails (or none present) falls back to writing OSC 52.
- The IO edge reports the path taken, appending '(wl-copy)' or
  '(OSC 52 — paste may need terminal support)' to the yank status — a silent
  no-op becomes a visible, explained outcome. Payload is unchanged (the rendered
  Value); only where the bytes go changes.
- Selection is pure over injected probe/run/fallback closures (copy_via), so the
  probe order, no-helper fallback, and present-but-fails fallback are tested at
  the IO seam with no real process or terminal.

442 lib tests green; clippy + lean build clean.
The read-only-off-at-runtime message had drifted between the two Meta-command
dispatchers — the REPL named the escape hatch (--read-only / a profile), the
Workbench did not — so an operator saw different wording per Frontend.

Pull the three cross-Frontend operator-facing strings into pub consts in repl.rs
(the existing shared-vocabulary home, alongside SYSINFO_QUERIES): the read-only
refusal, the :watch-in-transaction refusal, and the :connect-aborts-transaction
warning. Both dispatchers reference them; each still adds its own error:/note:
prefix. The refusal rules stay inline (trivially identical), and the two
dispatchers stay separate by design (ADR 0010's non-blocking Workbench).

Pure hygiene — no behaviour change. 442 lib tests green; clippy + lean clean.
Durable record of the deepening review + grilling pass: three of four candidates
rejected on the deletion test / call graph (B and C are correct factoring; D is
locality-only), one shipped (A, demoted to a thin string dedupe). FINDINGS.md is
the record so a future review does not re-suggest the rejected candidates;
report.html is the visual companion (the skill writes it to /tmp, kept here on
request).
… 0012)

File issue 22: write an inert, fully-commented example config.toml on
interactive first run when none exists (TTY + MGCONSOLE_CONFIG_PATH unset
+ file-missing). Amend ADR 0012 to record the scaffold as first-run
population of ~/.mgconsole, not a change to its location or escape hatch.
The one in-flight query was 5 fields scattered down WorkbenchState (run +
running_statement/started/tag, plus running_buffer), kept mutually consistent by
hand across ~6 lifecycle functions; nothing prevented Idle + a stale running_*.

Carry the three per-statement facts inside the variant:
  Running { id, statement, started, tag }
so they cannot dangle while Idle — the 'all cleared on idle' invariant is now
structural (the data goes with the variant). RunState loses Copy; id-only callers
go through a new RunState::running_id() accessor, so the new payload never leaks
to them. running_buffer and pending stay top-level — they outlive one statement
(the submission / buffer-identity concern). :o keeps tag: None (no history entry).

Behaviour-preserving refactor: 231 workbench tests green (incl. a new test
documenting the folded payload + accessor); clippy + lean build clean.

Architecture review round 2, candidate 1 (.scratch/architecture-review).
The six overlays (help, cell-detail, export, search, completion, command line)
were six independent fields, with 'at most one open' upheld only by discipline.
The 'is a modal open' check was triplicated — the key cascade, the mouse
ignore-list, and the draw — and the mouse copy had drifted (it omitted
search/command_line), so a mouse-click on a result cell while in-result search
was open opened the cell-detail overlay *on top of it*: a reachable two-overlay
state.

Replace the six fields (+ help_scroll/detail_scroll) with one
  modal: Option<Modal>
so 'two overlays open' is unrepresentable. The key dispatch is one match on a
Copy ModalKind tag; the draw matches &state.modal and renders exactly one; the
mouse is ignored whenever state.modal.is_some() — one check, all six, can't
drift — which fixes the bug structurally. drawer (a side panel) and watch (a
'press any key to stop' mode) stay separate; they are not modals.

Read/write views (search()/completion()/command_line()/detail()/…) keep the
handler call sites close to before while the storage stays the one enum.
recall_command uses the inline pattern (disjoint field borrow) where the method
accessor would clash with the command-history borrow.

444 lib tests green (incl. a regression test for the two-overlay bug); clippy +
lean build clean. Architecture review round 2, candidate 2.
Workbench-reducer sweep. #1 (fold the in-flight query into RunState::Running) and
#2 (one Modal sum type for the six overlays, fixing a reachable two-overlay bug)
shipped; #3 (drop the Buffer parking dance) deferred with its grouped
active_buffer shape recorded. Draw edge + untouched Core came back clean.
…bench switch

Design from a grilling session: switch between the two interactive Frontends
at runtime over a preserved Session, rather than the startup-only choice of
ADR 0010.

- ADR 0019: dispatch loop owns a Session-state bundle (params + Settings
  hoisted in to match the CONTEXT.md Session definition); Frontends return
  Quit | SwitchTo; :repl/:workbench Meta-commands; capability-gated :workbench
  (not --plain); refuse-while-busy reusing ADR 0005; lossy round-trip in v1.
- ADR 0010: amendment pointer (startup logic stands; choice no longer permanent).
- CONTEXT.md: Frontend and Session entries updated to reflect runtime switching
  and the params/Settings ownership the code now matches.
- .scratch/frontend-switch/: issues 01-03 (ready-for-agent) + 04 view-parking
  follow-up (needs-triage).
…rst interactive run

On the first interactive run with no config.toml and no MGCONSOLE_CONFIG_PATH
override, write a fully-commented EXAMPLE_CONFIG template to the resolved default
path so the format is discoverable. The template is inert (parses to
Config::default(), like a missing file) until the user uncomments it.

- config::EXAMPLE_CONFIG const + scaffold_example_config() write-if-missing,
  mirroring history::prepare_history_dir; a unit test proves the template stays
  inert and that a second run never clobbers an edited file.
- main::scaffold_config_on_first_run() gates on the default path (skips when
  MGCONSOLE_CONFIG_PATH is set) and is called only from the interactive path, so
  piped/run/-c/NDJSON invocations never write. A write failure is a warning.
… files (ADR 0020)

Replace the single tool-managed queries.toml with a tool-owned ~/.mgconsole/queries/
directory of plain <name>.cypher files. The directory is the source of truth — no
index — so saved queries are greppable, git-versionable, hand-editable, and the same
directory doubles as a :source library (a named query is a loadable script).

- queries.rs: resolve_queries_dir (was resolve_queries_path); NamedQueries keeps an
  in-memory mirror loaded from the directory + per-file sync_file() that writes one
  <name>.cypher (save) or deletes one file (forget), never a whole-store rewrite, so a
  crash mid-write leaves at most one bad file. Lenient parse (ADR 0020): an oddly-named
  or unreadable file is recorded as a warning surfaced by :saved, never fatal. Names
  with path separators are rejected (is_valid_name) so a name can't escape the dir.
- :save/:saved/:load/:forget unchanged in surface; the REPL calls sync_file and the
  Workbench reducer emits Effect::PersistQuery(name) (was the whole-store PersistQueries).
- queries.toml removed from the resolver and code; QUERIES_SUBDIR/QUERY_EXT replace
  QUERIES_FILENAME, with comments recording that ADR 0020 superseded the file.
- CONTEXT.md Named-query definition + ADR 0020 record the format change.

The verbs behave identically in the REPL and Workbench. No migration shim (pre-release).
… auth

Add an environment-variable password path so an authenticated connection can be
made non-interactively without leaking the password to shell history or ps.

resolve_password now takes the flag, env, and profile passwords as separate
sources rather than one pre-merged value, so MGCONSOLE_PASSWORD slots between the
explicit --password flag and a profile password:

  --password flag > MGCONSOLE_PASSWORD > profile password > hidden prompt

The first non-empty source wins. An empty username (anonymous) ignores the env
var entirely — no prompt, no auth. The env value is read once at the call site
and passed in, keeping resolve_password pure and testable. Deliberately not an OS
keychain (fragile over SSH/containers/CI). Documented in the example config and
the resolver doc; never echoed or logged. Unit tests cover the full precedence
and the anonymous-ignores-env case.
…rved Session

Stand up the dispatch loop above the two interactive Frontends (ADR 0019) and wire
the first direction: typing :repl in the Workbench drops to the line REPL with the
same live Session underneath — connection, open Transaction, params, Settings,
active Database, and Read-only mode all intact.

- frontend::Outcome { Quit, SwitchTo(Frontend) }: each Frontend's run-function now
  returns this instead of Result<()>; main's dispatch loop exits on Quit or
  re-enters the named Frontend on SwitchTo.
- Session bundle owned by the loop: the :param store and Settings are hoisted out
  of repl::run_loop and WorkbenchState into &mut-lent locals (CONTEXT.md's Session,
  made honest). The Session is shared via Arc<Mutex<Session>> so the connection/tx/
  read-only/database stay live across the switch; SessionRunner locks it per op.
- :repl is a cross-frontend Meta-command: in the Workbench it emits Effect::SwitchTo
  (refused while a query is in flight with the one-live-result "cancel first"
  guard, ADR 0005, without aborting the query); in the REPL it is a gentle no-op
  ("already in the REPL"). Added to the Workbench :help and command vocabulary.
- The Workbench aborts and awaits every Session-holding background task (schema
  fetch, param eval) before returning, so no straggler races the REPL's queries.
- HistoryFile is Clone so the loop lends a copy to each re-entered Frontend.

Unit tests cover the :repl parse, the REPL no-op, and the Workbench switch effect +
busy refusal. Up-switch (:workbench/:tui) and editor-text carry are issues 02/03.
Close the runtime Frontend-switch round-trip (ADR 0019): typing :workbench (or
its :tui alias) in the REPL raises the full-screen Workbench over the same live
Session — connection, open Transaction, params, Settings, active Database, and
Read-only mode all intact, in one fresh Buffer.

- MetaCommand::Workbench + meta_command parses :workbench/:tui, but only under the
  `tui` feature: a lean REPL-only binary has no Workbench, so the word reads as an
  unknown command (mirrors the feature-gated Workbench Frontend variant).
- Capability gate, not the --plain flag: ReplConfig gains `supports_tui` (a capable
  tty + the feature), threaded from main. dispatch_meta returns MetaFlow::Switch
  (→ Outcome::SwitchTo) when capable; on an incapable terminal (TERM=dumb / non-tty)
  it is refused with a clear message and the REPL carries on (the universal floor).
  Independent of --plain, so launching --plain on a capable terminal upgrades.
- :workbench typed in the Workbench is a gentle no-op ("already in the workbench"),
  the mirror of :repl in the REPL. A switch command inside a :source batch is a
  no-op (no mid-batch switch).
- :workbench listed in the REPL :help (feature-gated) and the Workbench command
  vocabulary.

Unit tests cover the parse (and the non-tui not-recognised contract), the capable
switch, the incapable refusal, the help listing, and the Workbench no-op.
The "query editor is connected" headline (ADR 0019): a draft you were composing
follows you across a Frontend switch instead of being lost.

- Down (Workbench → REPL): WorkbenchExit carries the active Buffer's editor text;
  the dispatch loop hands it to run_loop as the carried-down `initial`, which seeds
  the first prompt's input line (a multi-line draft survives intact). The meaningful
  direction — a half-drafted query arrives ready to edit or run.
- Up (REPL → Workbench): run_loop returns its current input line alongside the
  Outcome; the loop seeds the one fresh Buffer via WorkbenchConfig::initial_editor
  (new EditorState::with_text). Usually empty (the :workbench command consumed its
  own line) — carried for symmetry, not special-cased.

The dispatch loop holds a single `carry` String threaded between Frontends. Only
the active editor text travels; inactive Buffers and per-Buffer Result history stay
discarded (lossy round-trip; non-destructive view-parking is issue 04).

Unit tests cover the down-seed (incl. multi-line and the empty/no-spurious case),
the empty up-carry on a :workbench switch, and the Workbench seeding the fresh
Buffer from a carried draft (incl. that it is runnable, not a frozen seed).
Unblocked now that :repl/:workbench and editor-text carry have landed. Add a
codebase-grounded agent brief: Buffers are already purely Frontend-local, so
view-parking is the whole-Vec version of the existing take_active/install_active
move and the "rebind to live Session" reconciliation is automatic.
…witch

Make Workbench → REPL → Workbench round-trips non-destructive (ADR 0019 follow-up):
the whole Workbench view — every Buffer (editor text + per-Buffer Result history)
and the active index — is parked across the switch and restored on re-entry, rather
than rebuilding one fresh Buffer.

- WorkbenchView { buffers, active } parked by the dispatch loop. WorkbenchState
  gains detach_view/install_view — the whole-Vec form of the existing
  take_active/install_active per-Buffer park/restore.
- WorkbenchExit carries view: Some(detach_view()) on a SwitchTo, None on Quit
  (carry read before detach, which moves the active editor out). workbench::run
  takes an Option<WorkbenchView> and installs it over the freshly-built state.
- main's dispatch loop holds parked_view: on entry a parked view restores (and the
  empty up-carry must not clobber the restored active Buffer's text), else the one
  Buffer is seeded from the carry (issue 03); on exit parked_view = exit.view.

Buffers hold no session state, so reattaching onto the live Session needs no
per-field reconciliation — the ADR's feared "rebind to a snapshot" can't arise.

Also rebind the transaction marker from the live Session on entry (new
WorkbenchConfig.tx via Session::transaction_state), so a transaction opened before
a switch shows on re-entry instead of reading as autocommit. The active-Database
marker rebind is a documented small follow-up (Session exposes no db getter).

Tests: a reducer-level park→install round-trip preserves buffers/history/active and
the inactive Buffer's text; the tx marker seeds from config on entry. ADR 0019
updated to record the follow-up shipping.
…de2698c'

git-subtree-dir: experiment/rust
git-subtree-mainline: bbf7bda
git-subtree-split: e149e17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant