← Qnative (provisional) · c3

review-0.md

Review stop 0: Measure and seam

2026-11-11. Design c138 Phase 0 (../design/cycles/c138.developer-mode/plan.md, row 0 of the Phases table; "The gate", "Allowed, and on", "The seam, and the burden test"; Rulings 1 and 8). Built on 7468f9c.

In short

  • B holds: the switch and the lean build. All four lines of the burden test pass. Line 3 passes on this Mac: both variants build, the tests and stories pass, and the lean check passes. CI has never run, because the repository has no remote (ruling 7).
  • What the lean build saves: 1.07 MB of binary (23.2 MB against 24.3 MB, 4.4%) and nothing measurable at rest. Every quiet idle reading of every state with no Dev page shown sits between 60.3 and 63.0 MB, at 0.002–0.006% of a core and about one wake a second. Showing the Components page reads 65.0 and 65.3 MB. This matches the plan's estimate (1 to 2 MB of binary, close to nothing at rest); now it is a reading.
  • Developer mode is now an extension the app does not know about. A new crate, devmode, holds Tools and Components. The app takes it only with --features dev, through one line in main.rs. The default build is lean.
  • Allowed versus on is built. Allowed: debug builds, --dev or QPOINT_DEV=1, and only in the full build. On: a Developer mode checkbox in the tray menu, remembered across restarts. Not allowed means no group and no menu item.
  • The eager construction is fixed. Dev pages are built when opened and dropped when left, like every other page.

Rulings (2026-11-11)

Calls 1–4 were walked with Mark the same afternoon; he chose each recommended answer. That night he ruled to move through c3's blockers: Phases 1–3 run in sequence without stopping, and the rest of these calls are taken by their recommended answers until he overturns one (design 9eec43f656).

#CallRuled
1The gateB kept: the switch and the lean build. Still open to the Rust expert's review (ruling 1).
2The toggle's seatThe tray menu now; the app-menu item comes with V1's menu bar.
3Does --dev turn it on?No: --dev allows, the saved choice decides on.
4Leaving a Dev page drops itAccepted: one lifecycle for every page; Components forgets the open doc.
5cargo run -p app is leanA cargo dev alias (.cargo/config.toml): cargo dev -- --data synthetic.
6The expertStays open: who and when are still to be named.
7The atlasPublished: thread p0-seam under qnative c3.
8A config key that forbids developer modeStays proposed; an ask for the sensor's engineer in the Guide.
9The launch that quit by itselfWatch it; the runner records the exit status if it recurs.
10Idle readings need an idle MacIn the README's recipe, as part of R7.

How to try it

cargo run -p app --features dev -- --data synthetic    # developer mode allowed (debug), on
cargo run -p app -- --data synthetic                   # the lean build: no Dev group, no tray item
scripts/check-lean.sh                                  # builds both release variants and proves the lean one carries no developer mode

In the full build, the tray menu has a Developer mode item with a check. Clearing it removes the Dev group. If a Dev page is showing, the window goes to the overview. The choice is saved in ~/Library/Application Support/io.qpoint.desktop/settings.json; QPOINT_SETTINGS=<file> moves the file.

What was built

PieceWhereWhat it does
The protocolcrates/layer/src/extension.rsAn Extension is an id, a group title, the toggle's words and a list of Page { path, label, icon, shown, build }. It lives in the layer because devmode cannot depend on the app: the app depends on devmode, and Cargo refuses a cycle between packages. The component docs use the same shape (layer::demos::Doc).
The registrycrates/app/src/extensions.rsKeeps the extensions the binary was given and the person's on/off choice for each. Answers the shell (which groups to draw) and the router (which page a path names). Turning an extension off while its page is showing goes to the overview.
One routecrates/app/src/router.rsRoute::DevTools and Route::DevComponents are gone. Route::Ext(path) addresses any extension page. Route::resolve(path, cx) finds an extension's page where from_path finds the app's own.
The shellcrates/app/src/shell.rsNo Dev entities at startup. The sidebar draws Inventory, then one group per extension that is on. An extension page is built through the same one-page slot as an Inventory page, so leaving it drops its view.
Developer modecrates/devmode (new)Tools and Components, moved from crates/app/src/dev.rs unchanged apart from where their state lives. allowed() decides from the build and the flags. if_allowed(..) returns the extension or nothing. It depends on gpui-kit and layer only.
The one gatecrates/app/src/main.rs:79#[cfg(feature = "dev")] extensions.extend(devmode::if_allowed(repo.describe(), cx));
The featurecrates/app/Cargo.tomldevmode optional; [features] dev = ["dep:devmode"]. Not in default.
The appearance holdcrates/layer/src/theme.rsAppearance and the "follow the system unless something holds one" rule moved from the Dev section to the layer. The shell and the Tools page share it without either knowing the other.
The toggle's seatcrates/app/src/tray.rsA checked item per extension, between Open and Quit, with the words the extension gives. None in the lean build.
Saved choicescrates/app/src/settings.rs (new)The app's first settings file: {"extensions": {"dev": {"on": false}}}. Read once at startup, written when a choice changes. c2's D16 world choice goes beside it (agreed with the c2 session).
The lean checkscripts/check-lean.sh (new)Builds both release variants. Checks that devmode is absent from the lean dependency graph and present in the full one, and that eight strings found only in developer mode and the component docs are absent from the lean binary and present in the full one. The full binary is the control: a marker it lacks proves nothing.
CI.github/workflows/build.ymlAdds the app's library tests and the lean check on all three OSes. Not yet run (no remote).
A spike hookmain.rsQPOINT_SPIKE_OPEN=<path> opens a page by path at startup. Used for the "Components shown" reading.
Storiescrates/storiesapp-dev-tools and app-dev-components now go through Route::Ext. New: app-landing-lean, app-landing-dev-off (identical to the lean one) and app-dev-tools-off (a Dev page asked for while off lands on the overview). Each app story now starts with Info View off. Before this, a story captured after app-info-* inherited its open Info pane (the Dev stories did).
RecordsREADME.md (Run it), guide/02-run-it.mdThe default build is lean; --features dev for developers.

The readings

Method: the README recipe (Footprint).

  • Builds: release builds of this phase's code, lean and full, and of 7468f9c for comparison, each copied under a fresh file name.
  • Launch: QPOINT_DATA=synthetic, a scratch settings file (QPOINT_SETTINGS), the window on the first page, the guide on (its default).
  • Recording: 5 s to settle, then strobe 0.26.0 attached by PID for 60 s, with footprint and ps as the cross-check. footprint agreed with strobe's memory.app.private to the megabyte in every reading.
  • Machine: Apple M2 Max, macOS 15.6 (24G84), on AC power. The window opened on the main display, the Studio Display at 5120 × 2880.
  • Order: two rounds, every state in each, interleaved.
  • Recorded: 2026-11-11, 09:10–09:36.

An idle reading means an idle machine. The first reading was taken while the Mac was in use: the launch took focus under a live pointer and read 145 MB at 3.9% of a core. It and the one after it were discarded. From then on, a reading started only after 60 s with no keyboard or pointer input (HIDIdleTime). It counted only if there was still no input when it ended, the display was on and the session unlocked; otherwise it was taken again. Four more readings were thrown out that way (62 to 112 MB, 0.05 to 1.7% of a core). The table holds only the ones that counted.

StatePrivate memoryRSSCPU at restWakes/sFirst renderBinary
1. Lean (default build)61.6 · 63.0 MB76 · 77 MB0.0055% (3.3 ms in 60 s) · 0.0042% (2.5 ms)0.95 · 0.90260 · 277 ms23,230,096 B (23.2 MB)
2. Full, off (--features dev, no --dev: not allowed)77.4* · 61.0 MB79 · 76 MB0.033%* · 0.0037% (2.2 ms)2.73* · 0.98257 · 266 ms24,302,656 B (24.3 MB)
3. Full, on, nothing opened (--dev; the Dev group in the sidebar, the overview showing)63.0*† · 61.5 MB78 · 76 MB0.022%* · 0.0046% (2.8 ms)2.78* · 1.27416† · 279 ms24.3 MB
4. Full, Components shown (--dev, QPOINT_SPIKE_OPEN=/dev/components)65.3 · 65.0 MB80 · 81 MB0.0049% (3.0 ms) · 0.0050% (3.0 ms)1.18 · 0.92265 · 297 ms24.3 MB
Extra: full, allowed, turned off in the tray62.8 · 61.8 MB77 · 76 MB0.0039% (2.3 ms) · 0.0061% (3.7 ms)0.92 · 0.87291 · 276 ms24.3 MB
For comparison: 7468f9c (before this phase), release, no --dev60.3 · 63.0* MB76 · 77 MB0.0022% (1.3 ms) · 0.040%*0.90 · 4.31*282 · 284 ms24,218,976 B (24.2 MB)
  • * The window drew during that minute with no input. strobe names the display only when the process drew on the GPU, and in these readings it did. Wakes rise to 2.7–4.3 a second. Once memory rose too: 77.4 MB, all of it in IOSurface (38 MB against 25 MB, seven surfaces against five). Heap and code pages were the same as lean's. Each of these states also has a quiet reading. Nothing in the app asks for those frames at idle; what did was not found.
  • † The guide's port was held by another process during that launch, so the guide did not bind. The first render was 416 ms while the machine's load average was 3.8.
  • Not in the table:
    • A third "turned off" reading (60.8 MB) is excluded: a second window, a diagnostic launch of mine, was running during it.
    • The first "Components shown" launch quit by itself about 10 s in. It printed no panic and left no crash report. launchservicesd logged a quit, and none of the app's own quit paths (the tray, the spike hook) logged. It did not recur in three more launches: one diagnostic, then 65.3 and 65.0 MB. It is recorded as unexplained (open call 9).
  • Recordings, for strobe query <rec> …: lean rec_dbdd02, rec_2ca89b; full off rec_1cc0a8, rec_fe426c; full on rec_0773d9, rec_15ab21; Components rec_fc1c72, rec_1219d3; turned off rec_f9bf3c, rec_e612b3; 7468f9c rec_5055de, rec_1e5df4.

What the readings say:

The question (Guide page 7)The answer, read
What the lean build buys (1 against 2)1,072,560 bytes of binary, 4.4% of the full build. At rest, nothing measurable: full-off's quiet reading (61.0) sits within 2 MB of lean's two (61.6, 63.0), inside the launch-to-launch spread.
What carrying the code costs (2 against 1)The same answer from the other side: about 1.07 MB on disk, nothing resident that a reading can see, and no CPU (0.0037% against 0.004–0.0055%).
What the Dev group costs (3 against 2)Nothing measurable: 61.5 MB at 0.0046%, against 61.0 MB at 0.0037%. The group is two sidebar items.
What looking costs (4 against 3)About 3.5 to 4 MB while the Components page shows its first doc (65.0 and 65.3 against 61.5), and no CPU at rest. As in c2, showing a page costs more than carrying its code.
What turning it off buysTurned off reads like not allowed and like lean (62.8, 61.8 MB): nothing measurable.
What the eager construction costNot measurable. 7468f9c built both Dev views at startup and reads 60.3 MB quiet, in the same range. The fix is for the lifecycle rule (a page lives only while it is open), not for the budget.

Quiet readings range from 60.3 to 63.0 MB across launches of the same binaries, so differences under about 3 MB are not resolved by two readings per state. That is why the at-rest rows say "nothing measurable" rather than "zero".

The sentences Guide page 7 allows now: "The production build contains no developer tooling" (the lean check). "Where it is allowed, it does no work until it is switched on" (rows 2, 3 and the extra: no CPU at rest, no memory the readings can see). The third sentence waits for Phase 3's stream readings.

The burden test, line by line

Mark's condition: the gate must not mess up the structure or become a burden. Failing any line means we ship the switch alone (A) and say so.

1. The app crate contains exactly one cfg(feature = "dev"). Passes.

$ grep -rn 'cfg(feature' crates/app
crates/app/src/main.rs:13://! one `cfg(feature = "dev")` in this crate is the registration below.
crates/app/src/main.rs:79:            #[cfg(feature = "dev")]

Line 13 is the doc comment that says so; line 79 is the gate. The crate's other cfgs are cfg(test) on four test modules and cfg!(target_os) for the settings path; the tray backends are chosen by cfg(target_os) tables in its Cargo.toml. None is about developer mode.

2. No page outside devmode needs a cfg to compile lean. Passes. No cfg(feature …) exists anywhere in the workspace outside main.rs:79 (grep -rn 'cfg(feature' crates --include='*.rs'). The component docs (layer::demos, 2,250 lines) needed no gate either. Nothing in the lean binary reaches them, so the linker drops them: their three markers are absent from the lean binary and present in the full one. The stories crate depends on devmode always, needs no cfg, and draws the shell with developer mode on, off and absent.

3. Both variants build and pass their stories in CI, and a check proves the lean binary carries no dev strings. Passes on this Mac; CI not run.

  • Both variants build with no warnings, in debug (cargo build -p app, and the same with --features dev) and in release (the same with --release --locked).
  • Tests: cargo test --locked --workspace --exclude app: 32 passed, the stories crate's 10 among them (the seven doc-page click tests and the title-bar drag tests). cargo test --locked -p app --lib: 11 passed, 3 of them new for the settings file (a choice survives a restart; an unreadable file means nothing was chosen; in-memory settings write nothing).
  • Stories: cargo run --release -p stories captured all 85 stories in light and dark (170 PNGs) with no failures. app-landing-lean and app-landing-dev-off are byte-identical in both modes (SHA-256 of the PNGs), so developer mode turned off draws exactly the lean build's window. app-dev-tools-off asks for /dev/tools while developer mode is off and lands on the overview with no Dev group.
  • The lean check passes on the final code:
devmode in the dependency graph: lean 0, full 1  ok
"Developer mode"                                          0      1  ok
"Component demos"                                         0      1  ok
"GPUI's element inspector"                                0      1  ok
"Follow the system's light or dark setting, or hold one while reviewing"      0      1  ok
"/dev/components"                                         0      1  ok
"Every web component the Inventory section uses"          0      1  ok
"Our 21 roles, light and dark"                            0      1  ok
"The kit's components together in the Qpoint theme"       0      1  ok
lean 23,230,112 bytes · full 24,302,672 bytes · lean check: passed
  • The first run of the check failed, and the failure was useful. The marker "Debug builds only" (17 bytes) was missing from the full binary: the optimiser had built that string in registers instead of storing it whole. The control caught it, and the markers are now long strings. A lean binary that lacked the markers for the same reason would have passed falsely; the control rules that out.
  • What is not proved yet: the workflow has never run, so Linux and Windows are unproven, and so is the claim that their linkers strip the component docs as Apple's does (see the expert's question 3). It runs once the repository exists.

4. Moving today's Tools and Components behind the seam takes one sitting, and leaves the Inventory pages untouched. Passes.

  • One sitting. The first edit was at 08:55:59. The lean release build began at 08:59:57, after both debug variants compiled clean and the workspace tests passed. Reading the code beforehand took part of the same session.
  • The size of it: 20 files, +815 / −71 lines before the records. Most of that is new: the devmode crate (257 lines, mostly the moved dev.rs), the registry (90), settings (80, then tests), the protocol (65), the check (61). dev.rs (245 lines) is deleted.
  • The Inventory pages are untouched: no page file changed. The one line under crates/app/src/pages/ is the dispatcher's arm for the dev routes in pages/mod.rs:
-        Route::DevTools | Route::DevComponents => return None,
+        Route::Ext(_) => return None,
  • The guide, the tour and the app map needed no change: none of them named a Dev page.

Verdict: B, the switch and the lean build, as ruled. Nothing has to be forced: no line needed a workaround, and the one surprise (the marker) was the check doing its job. The switch-only fallback (A) is not needed.

For the Rust expert

The gate is a working decision made by people who are not Rust engineers (Mark, ruling 1: "switch + lean build seems right to me, but let's document as an un-informed decisions for a rust expert to be sure to review in case they want to follow a different course for other reasons"). Here is what was built and the questions we would like judged. The five questions in Guide page 1 stand; this adds what Phase 0 found.

  1. Where the protocol lives. devmode cannot depend on app: app depends on devmode optionally, and Cargo refuses a cycle between packages even across lib and bin. So the protocol (Extension, Page) sits in layer, and so does the appearance hold the Tools page and the shell share. Would a small dedicated crate (shell-api, ext) be cleaner than putting app plumbing in a component crate?
  2. Shared app state across the seam. An extension cannot read the app's globals. For example, RepoHandle, the process's one data nozzle, is in app::router. Today devmode is handed the data source's description as a string when it registers. Phase 2's Sensor page must swap the nozzle (c131 D16), so RepoHandle has to move somewhere both crates see (nozzle behind a gpui feature, the layer, or the crate in question 1). Which one?
  3. Dead-stripping is what removes the component docs. layer::demos is compiled into layer in both variants and has no gate. It is absent from the lean binary because nothing reaches it: thin LTO, codegen-units = 1, strip = true, and Apple's ld -dead_strip. Is that dependable on Linux (--gc-sections) and Windows (/OPT:REF), or should layer gate demos behind its own feature that devmode turns on? The check will tell on CI; we would like a view before relying on it.
  4. Feature unification. The lean build is cargo build -p app. If any workspace member ever depends on app with features = ["dev"], a cargo build --workspace would quietly be full. The stories crate avoids this: it depends on devmode directly, not on app/dev. Is a guard worth having, such as a cargo tree check in CI (the lean check already runs one per variant) or a compile_error! in a release profile?
  5. Teardown. An extension page is a view in the shell's one page slot, so leaving it drops the entity. Guide question 4 asks whether dropping an entity is a dependable way to close a stream. Phase 0 has no stream yet, but every Dev page now lives this way. A side effect: the Components page no longer remembers which doc was open after you leave it.
  6. The proof. CI builds both variants and checks the dependency graph and the strings. Is there a stronger check, for example symbols in an unstripped lean build, or cargo bloat crate shares?
  7. The default. Lean by default means cargo run -p app no longer shows the Dev pages, even in debug. Developers now type --features dev. Is a Cargo alias (.cargo/config.toml) the usual answer, or is it better to keep the friction visible?
  8. The tray toggle. It is a muda CheckMenuItem. On macOS, muda flips the check itself on a click; the app then sets it to the state it holds. Linux (ksni) and Windows are not exercised.

Open calls for Mark

  1. B confirmed: keep it? Recommended: yes. The readings and the four lines say it costs one seam and a second CI build, with no cfg creep.
  2. The toggle's seat (ruling 8, "to confirm at the Phase 0 review stop"). Built in the tray menu, which exists in both window and tray-only modes. The app has no menu bar yet, so the proposed app-menu item is not built. Add an app menu now, or when V1 adds one?
  3. Does --dev turn it on? Built: no. --dev makes developer mode allowed; the saved choice decides on. The first time, with nothing saved, it is on. Once the person turns it off, it stays off even with --dev. Keep that, or should a flag also switch it on?
  4. Leaving a Dev page drops it. The Components page forgets the open doc when you leave, like any page. The lifecycle rule (a feed lives only while its page is open) wants this for Live and Monitor. Accept it for Components too?
  5. cargo run -p app is lean now. It changes every developer's habit and every session's commands (the c2 session knows). Add a cargo dev alias, or leave it?
  6. The expert (open after the walk, 1): who and when. This file, Guide page 1 and the readings are what they would be shown.
  7. The atlas. The stories for this stop (lean, off, Tools, Components) were captured locally, not published to design's atlas. Publish them as thread p0-seam?
  8. A config key that forbids developer mode on a company's machines: still proposed, not built.
  9. One launch quit by itself. The first "Components shown" launch ended about 10 s in. No panic, no crash report, and none of the app's quit paths logged; launchservicesd logged a quit. Three more launches of the same binary in the same state ran their full minute. Unexplained. It is worth watching rather than chasing now: if it recurs, the runner records the exit status (measure2.sh: a signal against a clean quit).
  10. Idle readings need an idle Mac. Each launch takes focus. A reading is taken only after a minute without input and is retaken if anyone touches the machine. Should the recipe in the README say so? Proposed: yes, as a rule of R7.

Not done here

  • Phases 1 to 7. No stream, no Sensor or Config page, no Live or Monitor.
  • CI on three OSes: no remote (ruling 7; who creates the repository is open).
  • The app menu (open call 2).

Qpoint Brand Style Guide