Skip to content

Latest commit

 

History

History
328 lines (257 loc) · 21 KB

File metadata and controls

328 lines (257 loc) · 21 KB

machud product decisions — anti-drift anchors

These are the human owner's stated decisions, recorded to prevent drift during self-iteration. Don't quietly drift past them; if a proposed change would cross one of these boundaries, stop and ask first.

Vouching is per-decision: a heading carrying [VOUCHED @hyf0] is settled — reopen only on new evidence, a changed constraint, or @hyf0's explicit say-so. Unstamped decisions are AI-recorded on the owner's behalf — followed, but challengeable.

See also: architecture.md for the how.

D1. Opinionated & out-of-the-box — NOT configurable [VOUCHED @hyf0]

machud's whole reason to exist is the opposite of btop. btop wins on deep configurability; machud wins on beautiful + zero-config. A curated layout that looks good on first launch beats any settings system.

  • Do: pick sensible defaults, make them look great, ship them.
  • Don't: add config files, layout customization, per-user widget arrangement, or a "focus/expand" interaction mode. The single curated dashboard IS the product.

D2. Zero-sudo first, layered degradation [VOUCHED @hyf0]

Open-the-box means no password prompt on startup, ever. Default data comes only from unprivileged commands (sysctl, vm_stat, ioreg, pmset, netstat, df, ps) + Node os. Anything needing sudo (powermetrics: precise per-cluster freq, GPU/ANE watts, fan RPM, die temps) is shown as , never blocks, never prompts.

  • Do: keep the path sudo-free, period. There is no sudo mode — not even an opt-in --enhanced one. Never requesting a password is part of machud's identity (owner ruling 2026-06-20). Metrics that would need sudo/powermetrics (precise per-cluster freq, GPU/ANE watts, fan RPM, die temps, total system power) stay .
  • Don't: make the experience depend on elevated privileges, or add any code path — default or flagged — that shells sudo. A privileged helper is exactly the kind of fragile, security-sensitive surface this project refuses (see CONTRIBUTING.md / D1).
  • Display (owner ruling 2026-06-21): a metric that is permanently sudo-only (e.g. fan RPM) is omitted, not shown as a dead — sudo row — it never carries information here, so it's just clutter. (A metric that's merely momentarily absent still shows .) The UI never prints "sudo".

D3. Never crash, just degrade

A monitor that crashes is worse than one showing . sh() resolves "" on failure, each collector returns safe nulls, collectAll() swaps any throwing collector for its empty default.

  • Don't: introduce a code path where one bad reading blanks or crashes the app.

D4. Responsive — wide curated default + one narrow fallback (REOPENED 2026-06-20 by @hyf0)

Supersedes the old "wide-screen only, responsive deferred." @hyf0 reopened this (exactly the "reopen only with @hyf0" path the old decision reserved). machud now adapts to the terminal viewport size automatically, with no config or switch. Still zero-config, still one curated experience; it just fits the window. So it stays compatible with D1.

  • Scope = TWO tiers, not five. A wide curated default (the 3-tier hierarchy in DESIGN.md) + ONE narrow/watch-face single-column fallback, with a single tested breakpoint. The full 5-breakpoint × per-panel-S/M/L ladder is rejected as a scope bomb for the hands-off loop.
  • Build it LAST, after the visual redesign (RD5), and gate it on the verify width assertions.
  • Mind the width seam: useLayoutSize() exposes Runtime's reactive root-layout width. The --once/verify path has no TTY width, so the gate drives COLUMNS into both renderToString({ width }) and App's snapshot prop. Any responsive v-if must read that same width, so a breakpoint asserted at COLUMNS=120/40 is the width the code actually branches on.
  • Don't: let the loop self-grant responsive (it was a standing-anchor conflict — this reopen is the owner ruling that unblocks it). Don't add breakpoints the gate can't render-test.

D5. Scope — 9-module Stats parity as the goal, phased delivery

Target is feature parity with Stats' 9 modules, delivered in phases with quality per module (not all-at-once-half-baked).

  • Done: CPU (P/E split), Memory, GPU, Disk, Network, Battery, Sensors, Clock (in header).
  • Deferred: Bluetooth module, a standalone Clock module.
  • Do: when adding a deferred module, follow the existing collector→panel pattern (one collector file, one panel, props-only panel).

D6. Verify with real data, not guesses

Parsers are written against actual command output captured on a real Mac, not assumed formats. The black-box verification path is node dist/main.mjs --once; component panels also get pnpm test render coverage through @vue-tui/testing (see D20 / architecture.md).

  • Do: before changing a collector's parsing, run the real command and read its output. After any change, confirm with --once showing plausible live values.

D7. Take over the terminal like btop (alternate screen) [VOUCHED @hyf0]

The live dashboard must own the whole screen — switch to the terminal's alternate screen buffer on launch (clean buffer, hidden cursor, no leftover shell prompt or scrollback), and restore the user's terminal exactly on quit. Inline-below-the-prompt rendering was explicitly rejected — it looked like leftover junk and broke the "polished, app-like" feel.

  • How: mount with { mode: "fullscreen", exitOnCtrlC: true }, quit via useApp().exit(), and await app.waitUntilExit() so Runtime finishes restoration before the process ends. Signal teardown restores the terminal too. On non-TTY stdout, Runtime uses its supported document host; --once bypasses mounting and renders synchronously.
  • Don't: go back to inline rendering, or exit a live session with a raw process.exit() that leaves the terminal in the alternate buffer / cursor hidden.
  • Verify: capture raw bytes through a PTY (script) and confirm both \x1b[?1049h (enter) and \x1b[?1049l (exit) plus balanced cursor hide/show.

D8. One fixed foreground palette; the terminal owns the background

Owner ruling 2026-08-02: machud uses the palette shown in the current product screenshot for every session. It does not inspect macOS appearance, select a theme mode, or paint its own terminal background. The owner observed that this foreground palette works on both light and dark terminal backgrounds, so parallel palettes add product and implementation surface without adding value.

Appearance is not a system metric and is not carried in Metrics or --json. The runtime mirror is one plain object in src/theme.ts; DESIGN.md records the same fixed tokens and pnpm verify pins their complete key/value map.

D9. The visual identity is Vivid Curated, recorded in DESIGN.md

machud's look is cool but refined: drama comes from form (gradient meters, 2×4 braille graphs, big block numbers) and a confined Vivid Curated identity layer. The fixed palette uses green for CPU and the hud brand mark, magenta for GPU, electric violet for Memory, emerald/azure for Network, and electric cyan for Power. Disk and Status stay neutral until they alarm; amber/red remains reserved for events.

The terminal background is not part of the palette. The exact foreground tokens live in /DESIGN.md, with src/theme.ts as their runtime mirror and a full-map pin in scripts/verify.mjs.

The full spec lives in /DESIGN.md and is the hands-off aesthetic anchor the loop optimizes against — a [VOUCHED]-level surface. The staged RD0–RD5 backlog shipped it (2026-06-21): the code is now Everforest + the 3-tier layout, pinned by the verify gate.

  • Do: make src/theme.ts the runtime mirror of the DESIGN.md tokens, pinned in verify.mjs.
  • Don't: drift the look, or treat unbuilt DESIGN.md prose as a passing invariant.

D10. Opinionated contribution model

Owner ruling 2026-06-20, recorded in /CONTRIBUTING.md. Because machud is opinionated (D1), the contribution surface is intentionally narrow: bug/compat reports welcome; ideas as discussion-first issues; feature PRs opened without a prior agreed issue are closed directly; theming / feature / aesthetic changes are generally not accepted.

D11. Truecolor is an enhancement, not a guarantee — color-tier fallback

The gradient identity assumes 24-bit color, but macOS's default Terminal.app is 256-color. Without a fallback, same-hue ramps flatten to bands and muted hexes snap to saturated basic ANSI (the neon the design forbids). machud must detect the terminal color level and degrade: truecolor → full gradients; 256 → a single solid accent; 16 → muted named ANSI. Judge the look through the real render path at level 2/1, not raw truecolor.

D12. Network shows no IP address — recorded waiver

Owner ruling 2026-06-20. The NETWORK panel drops the LAN IP (previously shown) and shows interface name + rates instead. Rationale: a LAN IP is low glance-value and machud is built to be screenshotted/shared, so any network identifier is needless exposure. This is an explicit, logged waiver of DoD rule 3 (a previously-live metric going absent), not a silent regression.

D13. Distribution is npx machud — zero-install, the primary way people run it [VOUCHED @hyf0]

Owner ruling 2026-06-20. The common usage is npx machud — run-on-the-fly, no install. That makes the first five seconds the whole product and hard-reinforces the rest: zero-config (D1); never prompt for a password (D2 — a npx tool asking for root is a trust-killer); and the truecolor color-tier fallback (D11) is mandatory, because npx users are on whatever terminal they have, including macOS's default Terminal.app (256-color).

  • Do: keep the published package runnable via npx machud — un-private, a shebang on the bin, declared files/engines, and a lean runtime dep tree for fast cold start. A verify assertion must confirm the built bin actually launches (don't ship a package that can't run).
  • Don't: add a required install/build step, a heavy runtime dependency, or anything that makes the first npx machud slow, broken, or privilege-prompting.

D14. No accessibility layer — out of scope [VOUCHED @hyf0]

Owner ruling 2026-06-20. machud does not pursue accessibility. For a passive, full-screen TUI that takes over the terminal (D7), the realistic a11y surface is only colour-independence (screen-reader and keyboard a11y don't apply to a non-interactive alt-screen app), and the owner decided even that isn't worth it. So status is conveyed by colour (good/warn/bad) — no mandatory non-hue glyph, no colour-blind redundant-encoding requirement.

  • Do: use colour freely for status; a text label (e.g. disk "FULL") is fine where it reads well, but it's a UX choice, not an a11y obligation.
  • Don't: add ○/◐/●-style status glyphs, screen-reader hints, or other a11y scaffolding "to be safe." This is a deliberate, settled non-goal — reopen only with @hyf0.
  • Note: ⇡/⇣ (charge direction) and (unavailable) stay — they carry real information and replace a double-width emoji; they are not a11y features.

D15. The machud wordmark is two-tone mac|hud — mac silver, hud green

Owner ruling 2026-06-20. The top-left logo splits the word: mac in Apple aluminium/silver (the silver token — #8d939a) and hud in the brand green (the accent/cpu token — #8da101), surfacing the "a HUD for the Mac" reading.

  • Settled: the split (mac|hud), the silver mac, and the green hud. Reopen only with @hyf0. Mirrored in HeaderBar.vue (theme.silver + theme.accent, both bold) and the DESIGN.md silver token; an owner-directed extension of the D9 palette.
  • Don't: flatten the wordmark to one colour, swap which half is silver vs green, or drop the silver token.

D16. Theme switching is not product surface

Owner ruling 2026-08-02: machud has no theme key, theme configuration, appearance auto-detection, or preview override. q remains the only input advertised by the dashboard; the footer describes refresh timing but offers no appearance control. This keeps the product aligned with D1 and the single fixed-palette decision in D8.

D17. Component test layer — vp test re-enabled, panels rendered via renderToString

Historical implementation record. The decision to keep a component render layer remains; its Vite+ 0.1 / Runtime 0.1 constraints were superseded by D20 on 2026-08-02.

Owner asked (2026-06-21) to borrow vue-tui's verification layer. Two things shipped:

  1. vp test un-broken. It failed because the catalog used @latest, pulling vite-plus-core/CLI 0.2.1 while vite-plus-test has no 0.2.x (maxes at 0.1.24) — the split breaks the test bin ("Could not find 'vitest' bin entry"). Fixed the way vue-tui structurally avoids skew: pin the whole toolchain to one matched line in pnpm-workspace.yaml (vite→core@0.1.24, vitest→test@0.1.24, vite-plus@0.1.24). Bump all three together when test ships a newer release. vp build still passes on this line.
  2. Component render tests (tests/*.test.ts): each panel is rendered with injected metrics through the runtime's synchronous renderToString(component, {columns}) — the same call --once uses — and asserted on (title, headline value, degradation to , narrow view).

Why renderToString, not @vue-tui/testing's render()/lastFrame() (vue-tui's own harness): the published @vue-tui/testing@0.0.3 hard-pins @vue-tui/runtime@0.0.3, but machud runs runtime 0.1.0 — a mismatch with no matched testing release on npm. renderToString is exported by the runtime itself, so it matches 0.1.0 and needs no harness. Panels are pure props→frame; the only interactive surface (t/q) is already covered by the PTY checks in scripts/verify.mjs.

Config (vite.config.ts, defineConfig from vite-plus): FORCE_COLOR:"3" + CI:"false" (keep chalk colour + vue-tui interactivity on) and environment:"happy-dom". Vitest defaults to the node/SSR transform, which makes @vitejs/plugin-vue emit ssrRender; a browser-like env flips it to the CLIENT render fn that renderToString needs (vue-tui sidesteps this by testing in JSX, never importing SFCs — machud tests real .vue panels). pnpm verify runs vp test as its first step, so the layer is enforced by the single gate, not a side command.

D18. Dashboard reading order: CPU/GPU/MEM · NETWORK/POWER/DISK · STATUS [VOUCHED @hyf0]

Owner ruling 2026-07-05; amended by owner direction 2026-07-06. The wide dashboard's durable reading path is:

Top:    CPU | GPU | MEMORY
Middle: NETWORK | POWER | DISK
Bottom: STATUS

Why: the first row answers "what pressure is shaping the machine right now?" CPU opens the row on the left; GPU sits in the middle so the two compute-pressure channels can be compared directly; Memory moves to the far right. CPU now carries the largest panel width, Memory is second, and GPU remains smallest. GPU is allowed to be a first-tier hero with a large percentage, but it must not pretend to have CPU-style process attribution; it earns the space with current utilization, history/trend, renderer load, VRAM, and compact avg/peak context.

Tier-1 spatial weight is CPU > Memory > GPU, using the old 7/2/1 weight ladder reassigned to the new reading path. CPU must visually lead by width on the left; Memory is second on the right; GPU may be narrower because it does not have reliable zero-sudo per-process attribution.

The second row answers "which moving resources need attention?" Network owns the left slot because its rates change most frequently and deserve the highest scan position in that row. Power now sits before Disk (amended 2026-07-05): with the Vivid Curated palette, Power carries an electric-cyan identity (its story is 电) and becomes the second-row hero, while Disk trails as the low-drama capacity readout. Power remains visible as a compact environment box, but danger states override position: Disk full or low battery must escalate through colour/intensity/text without moving panels.

The bottom row is STATUS, not SENSORS. It summarizes health signals such as thermal pressure, CPU speed cap, and battery pack temperature. The title must not imply unavailable full sensor coverage (CPU/GPU die temps, fan RPM), which remain omitted under D2.

  • Colour grammar: warm colours (yellow/orange/red) are event-only, not module identity. Disk may become warm when near-full/full; Power may become warm on low battery / Low Power Mode; Status may become warm on thermal pressure. Roomy Disk and nominal Status stay neutral; Power now carries an electric-cyan identity full-time (still escalating to amber/red when low). Vivid Curated hues (D9 amend 2026-07-06): CPU green, GPU magenta, Memory electric violet, Power electric cyan, Network emerald ▼ / azure ▲ (stacked ▲ up over ▼ down).
  • Do: implement and verify this order in App.vue, DESIGN.md, component tests, and scripts/verify.mjs before calling RD6 complete.
  • Don't: return GPU to a Network peer, put Disk before Power in the default row order, move Memory away from the top-row right slot, or label the health summary SENSORS without an explicit owner reversal.

D19. Framed layout — cap the width and centre in the viewport (not full-bleed)

Owner ruling 2026-07-06. On a large, very tall, or oddly-shaped terminal the dashboard used to sit top-left at its natural size and full terminal width, so all the leftover space collected at the bottom as one dead gap (and on ultra-wide terminals the panels stretched into sparse, half-empty boxes). machud now frames the dashboard: it caps the content width (~128 columns) and centres the block in the viewport — horizontally always, vertically whenever the terminal is taller than the content. The leftover space becomes balanced margin around a composed block instead of a gap dumped at the bottom.

This is compatible with D1 (not configurable) because it is automatic and zero-config — the single curated dashboard just fits the window.

  • Rejected — stretch panels to fill the height (btop-style vertical flexGrow). machud's panels are curated (a hero number + a short graph + a 3-item list), not dense or scrollable. Growing them vertically only inflates the empty space inside each panel — a big number floating in a big box — which looks worse, not better. btop can stretch because its panels are dense; machud's are not.
  • Rejected — do nothing. Defensible for a generic monitor, but machud's whole pitch is "looks right out of the box" (D1), so a dead gap / stretched sparse panels undercut the product.
  • Implementation: App.vue's outer <Box> fills the terminal (columns × rows) and centres the inner, width-capped dashboard. MAX_CONTENT_WIDTH (128) is the single tunable knob. Vertical centring engages only when the viewport is taller than the content's (upper-bound) height, so a short terminal keeps the top-anchored layout and the header is never clipped. Height is threaded to the one-shot render via a rows prop (like columns), so the piped --once snapshot stays top-anchored (unknown height) while the gate can drive LINES to test centring deterministically.
  • Verify: width caps at COLUMNS=200; a tall viewport (LINES=45) centres (blank rows above the header); a short viewport (LINES=30) stays top-anchored (never clips); and the real content height is pinned as an upper bound so a panel growing past it turns the gate red before it could clip.
  • Note (unstamped): owner-directed and recorded, but not [VOUCHED] — challengeable until @hyf0 stamps it. The ~128 cap width is a taste knob, easy to tune.

D20. Vue-tui 0.3 application layout — Vite for development, tsdown for production

The 2026-08-02 migration adopts the current templates/vite boundary while preserving machud's product and verification contracts:

  • vite.config.ts is development-only: unplugin-vue/vite compiles SFCs and @vue-tui/vite supplies terminal HMR.
  • tsdown.config.ts is production-only: unplugin-vue/rolldown compiles SFCs and bundles the Node application plus dependencies into executable dist/main.mjs. Bundled libraries stay in devDependencies; generated third-party notices ship beside the bin.
  • vitest.config.ts loads only the Vue transform. Component tests mount real SFCs with matched @vue-tui/testing@0.3.0; no DOM environment, Vite+ compatibility pin, or synchronous-render workaround remains.
  • Runtime 0.3 owns fullscreen lifecycle, tagged input events, root layout size, explicit string-render dimensions, and the legal terminal-color type. machud uses those contracts directly rather than retaining 0.1 compatibility branches.
  • pnpm verify remains the authoritative strengthen-only gate and tests live resize, Ctrl+C restoration, the standalone file in an empty directory, and the installed tarball.

This supersedes D17's implementation constraints, not its reason for having component tests.