Make native app domain first

This commit is contained in:
saymrwulf 2026-04-28 13:32:16 +02:00
parent 56163922c0
commit 9704ac8452
6 changed files with 192 additions and 211 deletions

View file

@ -15,27 +15,12 @@ The first implementation is deliberately conservative:
## Quick Start
```bash
./scripts/ratchet setup
./scripts/ratchet
./scripts/ratchet app
```
`./scripts/ratchet` is the cockpit. It tells you exactly what to do next.
This builds and opens the native macOS control room. Use the app for normal operation; terminal commands are advanced fallback tools.
If the cockpit is in cooldown and you want the app to wait until the earliest next action, run:
```bash
./scripts/ratchet pipeline
```
It prints the exact monitor-only plan and asks `yes/no` before doing anything.
For the durable forever lifecycle supervisor:
```bash
./scripts/ratchet supervise
```
It persists lifecycle state in `data/ratchet.sqlite`. If the process crashes or the Mac reboots, start the same command again and it resumes from SQLite.
The lifecycle state persists in `data/ratchet.sqlite`. If the app or Mac restarts, open the app again and it reads the same state.
When you manually place a Braiins bid, record the exposure so the supervisor blocks new experiments:
@ -49,7 +34,7 @@ Close it only when finished:
./scripts/ratchet position close POSITION_ID
```
For the native macOS SwiftUI shell:
For the native macOS app:
```bash
./scripts/ratchet app
@ -57,9 +42,9 @@ For the native macOS SwiftUI shell:
This builds `macos/build/Braiins Ratchet.app` and opens the real app bundle. Do not use `swift run` for normal operation.
The app is a native visual control room: Mission Control, Research Map, Manual Exposure ledger, Reports, and a Ratchet Lecture. The design rationale is in `docs/APP_DESIGN_RESEARCH.md`.
The app is a native visual control room: Mission Control, Research Map, Manual Exposure ledger, Advanced diagnostics, and a Ratchet Lecture. The design rationale is in `docs/APP_DESIGN_RESEARCH.md`.
For a 6-hour monitoring session:
Advanced fallback for a 6-hour CLI monitoring session:
```bash
./scripts/ratchet watch 6

View file

@ -1,18 +1,12 @@
# Start Here
This project now has one operator entry point:
This project now has one normal operator entry point:
```bash
./scripts/ratchet
./scripts/ratchet app
```
That is the same as:
```bash
./scripts/ratchet next
```
It prints the cockpit: current state, exact next action, interpretation, reference commands, and ratchet rule.
That command builds and opens the native macOS app. The app is the control room. The terminal is only the launcher and fallback diagnostic path.
## Your Job
@ -20,113 +14,43 @@ Your job is not to understand every metric.
Your job is:
1. Run `./scripts/ratchet`.
2. Do only what it says under `DO THIS NOW`.
3. Ignore every other command unless `DO THIS NOW` tells you to run it.
4. If it tells you to run `./scripts/ratchet watch 2`, start it, leave the terminal open, and come back after about 2 hours.
5. If you manually place a Braiins canary, write down the order details outside this repo and wait through the maturity window before judging it.
1. Open the app with `./scripts/ratchet app`.
2. Stay on `Mission Control` unless you intentionally need raw diagnostics.
3. Read `Current Decision` first.
4. Read `Who Is In Control` second.
5. Use `Next Passive Action` only when it is enabled.
6. If you manually place a Braiins canary, record it in `Manual Exposure` immediately.
Do not start extra terminal watches while the app says a watch, cooldown, or manual exposure owns control.
## Who Is In Control?
If `watch` is running, the Python process is in control of that terminal.
The app has one ownership model:
You do not need to babysit it. It will:
1. `The app is ready`: you may start the enabled passive action.
2. `A watch run owns control`: leave it alone until it finishes.
3. `Cooldown owns control`: wait until the shown earliest action time.
4. `Manual exposure owns control`: supervise the real-world Braiins/OCEAN position and do not start new experiments.
5. `The app is busy`: a monitor-only backend operation is running right now.
1. Collect samples every 5 minutes.
2. Write the run report when it finishes.
3. Print the cockpit again.
4. Return control to your shell prompt.
This is the anti-babysitting rule: if the app says something else owns control, your workload is zero unless you are supervising a real manual exposure.
If you want the technical report, run `./scripts/ratchet report`. The normal workflow intentionally shows the cockpit first.
## What The App Does
After a watch finishes, the cockpit enters a post-watch cooldown. That is deliberate.
The app is monitor-only. It never places, modifies, or cancels Braiins orders.
Post-watch cooldown means:
It can:
1. The current experimental stage is complete.
2. Starting another identical watch immediately is not useful ratcheting.
3. The run report is the evidence artifact.
4. The next planned touch is a later fresh sample, usually `./scripts/ratchet once`.
During cooldown, the cockpit shows:
1. A progress bar.
2. The earliest next action time.
3. The remaining minutes.
## Controlled Automation
If you do not want to babysit the cooldown manually, run:
```bash
./scripts/ratchet pipeline
```
The pipeline first prints a proposal like:
```text
I am going to: wait until this time, run one fresh sample, print the cockpit, then stop.
Are you OK with this? Type yes or no.
```
It only runs after you type `yes`.
It is still monitor-only. It never places, changes, or cancels Braiins orders.
## Forever Supervisor
For the full autoresearch lifecycle, run:
```bash
./scripts/ratchet supervise
```
The supervisor is the long-running engine. It:
1. Loads persisted state from `data/ratchet.sqlite`.
2. Waits through cooldown if cooldown is active.
3. Runs the next passive watch when due.
4. Writes reports and lifecycle events.
5. Re-enters cooldown.
6. Repeats until you stop it.
If it crashes or the Mac reboots, start the same command again. It resumes from SQLite.
Use this to inspect persisted state without starting the loop:
```bash
./scripts/ratchet supervise --status
```
## Manual Braiins Exposure
If you manually start a Braiins bid, record it immediately:
```bash
./scripts/ratchet position open --description "Braiins order abc, 0.0001 BTC, 3h canary" --maturity-hours 72
```
While a manual position is active:
1. The cockpit says `HOLD`.
2. The supervisor blocks new watch experiments.
3. Restarting the app keeps the manual exposure state.
List positions:
```bash
./scripts/ratchet position list
```
When the Braiins/OCEAN exposure is truly finished:
```bash
./scripts/ratchet position close POSITION_ID
```
1. Read persisted lifecycle state from `data/ratchet.sqlite`.
2. Collect OCEAN and public Braiins market samples.
3. Run passive watch-only research windows.
4. Write run reports under `reports/`.
5. Track manually executed Braiins exposure that you enter yourself.
6. Resume from the same SQLite state after a crash or reboot.
## Native Mac App
The native SwiftUI shell is in:
The native SwiftUI app is in:
```text
macos/BraiinsRatchet
@ -140,34 +64,23 @@ Build and open the real app bundle:
This creates `macos/build/Braiins Ratchet.app`. After that, you can open that app bundle directly from Finder or pin it in the Dock.
The app is a native cockpit over the same durable Python lifecycle engine.
The app includes controls to record and close manual exposure, but the same rule applies: it never places Braiins orders.
The app is organized as:
1. `Mission Control`: one exact next action, cooldown, metrics, and direct watch-only controls.
1. `Mission Control`: current decision, control ownership, next passive action, progress, evidence, and plain English interpretation.
2. `Research Map`: visual autoresearch stage model.
3. `Manual Exposure`: record or close manually executed Braiins exposure.
4. `Reports`: raw cockpit, report, and ledger artifacts.
4. `Advanced`: raw cockpit, report, and ledger artifacts for diagnostics.
5. `Ratchet Lecture`: the general observe, hypothesize, bound, mature, adapt method.
## Research Pathway
The cockpit has two different time horizons:
The app has three time horizons:
1. `DO THIS NOW` is the only command you should run next.
2. `Ratchet Pathway Forecast` tells you what the next stages probably look like.
1. `Immediate`: what can happen now, usually start, wait, refresh, or hold.
2. `Midterm`: what probably happens after the current watch, cooldown, or manual exposure matures.
3. `Longterm`: what could happen after multiple evidence artifacts point in the same direction.
The forecast is not a profit prediction. It is a workload and research-flow prediction.
It is split into:
1. `Immediate`: what happens now.
2. `Midterm`: what probably happens after the current run or sample.
3. `Longterm`: what could happen after multiple reports mature.
Expect the pathway to change after each report. That is the point of ratcheting: the next stage adapts to measured evidence instead of following a rigid plan.
The pathway is allowed to change after each report. That is the point of ratcheting: the next stage adapts to measured evidence instead of following a rigid plan.
## What The Actions Mean
@ -177,7 +90,7 @@ Expect the pathway to change after each report. That is the point of ratcheting:
`manual_bid` means the stricter profit-seeking guardrails cleared. The code still does not place the order. You decide manually in Braiins.
## Where The Reports Are
## Where The Evidence Lives
The master ledger is:
@ -191,12 +104,24 @@ Each completed watch creates one run report:
reports/run-*.md
```
Older sessions can be embedded with:
Use the app's `Advanced` tab when you need raw artifacts. Mission Control intentionally hides raw logs during normal operation.
## Advanced Fallback Commands
Use these only if the native app cannot be opened or you are debugging:
```bash
./scripts/ratchet retro START_UTC END_UTC
./scripts/ratchet
./scripts/ratchet once
./scripts/ratchet watch 2
./scripts/ratchet supervise
./scripts/ratchet position list
./scripts/ratchet report
./scripts/ratchet experiments
```
The preferred workflow remains the native app.
## The Ratchet Rule
One run is not a verdict. One run is a measurement.

View file

@ -48,7 +48,7 @@ The SwiftUI app turns that into native surfaces:
- `Mission Control`: one exact action, cooldown, direct watch-only controls, and metrics.
- `Research Map`: the ratchet pathway as a visual stage model.
- `Manual Exposure`: the ledger for real manually placed Braiins exposure.
- `Reports`: raw artifacts kept available but no longer primary.
- `Advanced`: raw artifacts kept available but no longer primary.
- `Ratchet Lecture`: a teachable model of observe, hypothesize, bound, mature, adapt.
## The Ratchet UX Rule

View file

@ -1,8 +1,8 @@
# Braiins Ratchet Mac
Native SwiftUI shell for the durable Braiins Ratchet lifecycle engine.
Native SwiftUI control room for the durable Braiins Ratchet lifecycle engine.
The Python supervisor remains the source of truth. This app reads the same repository-local SQLite state through `./scripts/ratchet`.
The Python lifecycle engine remains the source of truth. This app reads the same repository-local SQLite state through structured app state, not by making Mission Control a terminal transcript.
## Normal Run
@ -19,10 +19,10 @@ This builds `macos/build/Braiins Ratchet.app` and opens the packaged app. Use th
- Research Map with the full ratchet pathway.
- Direct watch-only controls without an approval gate.
- Manual exposure recording and closing controls.
- Reports panel for raw artifacts.
- Advanced panel for raw artifacts and backend diagnostics.
- Ratchet Lecture for the general autoresearch method.
- Monitor-only. It never places Braiins orders.
## Product Direction
The next production step is wiring LaunchAgent controls for the durable supervisor.
The next production step is wiring LaunchAgent controls for the durable supervisor while keeping Mission Control domain-first.

View file

@ -79,7 +79,6 @@ struct ContentView: View {
case .mission:
MissionControlView(
appState: appState,
transcript: transcript,
isRunning: isRunning,
glow: glow,
refresh: { Task { await refreshAppState() } },
@ -98,8 +97,8 @@ struct ContentView: View {
close: closeManualExposure,
list: { Task { await runTextCommand(label: "position list", ["position", "list"], refreshAfterwards: true) } }
)
case .reports:
ReportsView(
case .advanced:
AdvancedView(
transcript: transcript,
lastCommand: lastCommand,
isRunning: isRunning,
@ -224,7 +223,7 @@ enum AppSection: String, CaseIterable, Identifiable {
case mission
case map
case exposure
case reports
case advanced
case lecture
var id: String { rawValue }
@ -234,7 +233,7 @@ enum AppSection: String, CaseIterable, Identifiable {
case .mission: "Mission Control"
case .map: "Research Map"
case .exposure: "Manual Exposure"
case .reports: "Reports"
case .advanced: "Advanced"
case .lecture: "Ratchet Lecture"
}
}
@ -244,7 +243,7 @@ enum AppSection: String, CaseIterable, Identifiable {
case .mission: "scope"
case .map: "point.3.connected.trianglepath.dotted"
case .exposure: "lock.shield"
case .reports: "doc.text.magnifyingglass"
case .advanced: "wrench.and.screwdriver"
case .lecture: "graduationcap"
}
}
@ -252,7 +251,6 @@ enum AppSection: String, CaseIterable, Identifiable {
struct MissionControlView: View {
let appState: AppStatePayload?
let transcript: String
let isRunning: Bool
let glow: Bool
let refresh: () -> Void
@ -268,6 +266,7 @@ struct MissionControlView: View {
VStack(spacing: 14) {
AutoresearchOrb(phase: ResearchPhase.from(appState), glow: glow)
.frame(height: 250)
ControlOwnershipCard(appState: appState, isRunning: isRunning)
PassiveRunCard(
plan: appState?.automationPlan,
isRunning: isRunning,
@ -277,9 +276,9 @@ struct MissionControlView: View {
.frame(width: 350)
}
MetricsDeck(appState: appState)
EvidenceDeck(appState: appState)
ResearchTimeline(appState: appState, compact: false)
PlainEnglishCard(appState: appState, transcript: transcript)
PlainEnglishCard(appState: appState)
}
.padding(28)
}
@ -312,7 +311,7 @@ struct HeroPanel: View {
}
VStack(alignment: .leading, spacing: 10) {
Text("Do This Now")
Text("Current Decision")
.font(.caption.weight(.heavy))
.textCase(.uppercase)
.foregroundStyle(.secondary)
@ -322,17 +321,6 @@ struct HeroPanel: View {
Text(directive.detail)
.font(.title3.weight(.semibold))
.foregroundStyle(.primary)
if let command = directive.command {
HStack(spacing: 10) {
Image(systemName: "terminal")
Text(command)
.font(.system(.body, design: .monospaced).weight(.semibold))
}
.padding(12)
.frame(maxWidth: .infinity, alignment: .leading)
.background(.black.opacity(0.18), in: RoundedRectangle(cornerRadius: 14, style: .continuous))
}
}
if let watch = appState?.operatorState.completedWatch {
@ -345,6 +333,53 @@ struct HeroPanel: View {
}
}
struct ControlOwnershipCard: View {
let appState: AppStatePayload?
let isRunning: Bool
var body: some View {
GlassPanel(padding: 16) {
VStack(alignment: .leading, spacing: 10) {
Label("Who Is In Control", systemImage: symbol)
.font(.headline)
Text(title)
.font(.title3.weight(.bold))
Text(detail)
.font(.callout)
.foregroundStyle(.secondary)
}
.frame(maxWidth: .infinity, alignment: .leading)
}
}
private var symbol: String {
if isRunning { return "gearshape.2" }
guard let state = appState?.operatorState else { return "questionmark.circle" }
if state.activeWatch != nil { return "binoculars" }
if !state.activeManualPositions.isEmpty { return "lock.shield" }
if state.completedWatch != nil { return "timer" }
return "scope"
}
private var title: String {
if isRunning { return "The app is busy" }
guard let state = appState?.operatorState else { return "Loading state" }
if state.activeWatch != nil { return "A watch run owns control" }
if !state.activeManualPositions.isEmpty { return "Manual exposure owns control" }
if state.completedWatch != nil { return "Cooldown owns control" }
return "The app is ready"
}
private var detail: String {
if isRunning { return "A monitor-only operation is running. Do not start a competing action." }
guard let state = appState?.operatorState else { return "Reading the lifecycle database." }
if state.activeWatch != nil { return "Let the watch finish; duplicate watches corrupt the research trail." }
if !state.activeManualPositions.isEmpty { return "A real-world position is active, so new experiments stay blocked." }
if let watch = state.completedWatch { return "Wait until \(watch.earliestActionLocal) before the next useful sample." }
return "No active watch, no manual exposure, and no cooldown block."
}
}
struct PassiveRunCard: View {
let plan: AutomationPlanPayload?
let isRunning: Bool
@ -353,7 +388,7 @@ struct PassiveRunCard: View {
var body: some View {
GlassPanel {
VStack(alignment: .leading, spacing: 14) {
Label("Watch-only Control", systemImage: "binoculars")
Label("Next Passive Action", systemImage: "arrow.forward.circle")
.font(.headline)
Text(title)
.font(.title3.weight(.bold))
@ -419,7 +454,7 @@ struct PassiveRunCard: View {
switch plan.kind {
case "once_now": return "Refresh Now"
case "watch_2h": return "Start Watch-only Run"
case "wait_then_once": return plan.waitSeconds > 0 ? "Wait" : "Refresh Now"
case "wait_then_once": return plan.waitSeconds > 0 ? "Cooldown Active" : "Refresh Now"
case "report_only": return "Open Report"
default: return "Refresh State"
}
@ -438,43 +473,50 @@ struct PassiveRunCard: View {
}
}
struct MetricsDeck: View {
struct EvidenceDeck: View {
let appState: AppStatePayload?
var body: some View {
LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 14), count: 4), spacing: 14) {
MetricTile(
title: "Market Freshness",
value: freshnessText,
detail: appState?.operatorState.latestMarketTimestamp ?? "no sample",
symbol: "clock"
title: "Braiins Market",
value: marketPrice,
detail: marketDetail,
symbol: "chart.line.uptrend.xyaxis"
)
MetricTile(
title: "Strategy Action",
value: appState?.operatorState.action ?? "none",
title: "Model Net",
value: proposalValue("expected_net_btc", fallback: "n/a"),
detail: actionDetail,
symbol: "target"
symbol: "plus.forwardslash.minus"
)
MetricTile(
title: "Manual Exposure",
value: exposureText,
detail: "Blocks new experiments while active",
symbol: "shield.lefthalf.filled"
title: "OCEAN Pool",
value: oceanValue("pool_hashrate_eh_s", suffix: " EH/s"),
detail: oceanValue("network_difficulty_t", prefix: "difficulty "),
symbol: "water.waves"
)
MetricTile(
title: "Latest Report",
value: appState?.operatorState.latestReport?.lastPathComponent ?? "none",
detail: appState?.operatorState.latestReport ?? "No artifact yet",
symbol: "doc.text"
title: "Evidence",
value: evidenceValue,
detail: evidenceDetail,
symbol: "archivebox"
)
}
}
private var freshnessText: String {
guard let state = appState?.operatorState else { return "loading" }
if state.isFresh { return "fresh" }
if let minutes = state.freshnessMinutes { return "stale \(minutes)m" }
return "unknown"
private var marketPrice: String {
if let fillable = appState?.latest.market?["fillable_price_btc_per_eh_day"]?.description, fillable != "n/a" {
return fillable
}
return appState?.latest.market?["best_ask_btc_per_eh_day"]?.description ?? "n/a"
}
private var marketDetail: String {
let freshness = appState?.operatorState.freshnessMinutes.map { "\($0)m old" } ?? "age unknown"
let ask = appState?.latest.market?["best_ask_btc_per_eh_day"]?.description ?? "n/a"
let last = appState?.latest.market?["last_price_btc_per_eh_day"]?.description ?? "n/a"
return "\(freshness), ask \(ask), last \(last)"
}
private var actionDetail: String {
@ -484,9 +526,26 @@ struct MetricsDeck: View {
return "No useful market action"
}
private var exposureText: String {
private var evidenceValue: String {
let count = appState?.operatorState.activeManualPositions.count ?? 0
return count == 0 ? "none" : "\(count) active"
if count > 0 { return "\(count) active exposure" }
return appState?.operatorState.latestReport?.lastPathComponent ?? "none"
}
private var evidenceDetail: String {
if let watch = appState?.operatorState.completedWatch {
return "cooldown \(watch.remainingMinutes)m remaining"
}
return appState?.operatorState.latestReport ?? "No artifact yet"
}
private func proposalValue(_ key: String, fallback: String) -> String {
appState?.latest.proposal?[key]?.description ?? fallback
}
private func oceanValue(_ key: String, prefix: String = "", suffix: String = "") -> String {
guard let value = appState?.latest.ocean?[key]?.description else { return "n/a" }
return "\(prefix)\(value)\(suffix)"
}
}
@ -617,7 +676,6 @@ struct TimelineNode: View {
struct PlainEnglishCard: View {
let appState: AppStatePayload?
let transcript: String
var body: some View {
GlassPanel {
@ -799,7 +857,7 @@ struct ActiveExposureList: View {
}
}
struct ReportsView: View {
struct AdvancedView: View {
let transcript: String
let lastCommand: String
let isRunning: Bool
@ -811,9 +869,9 @@ struct ReportsView: View {
VStack(alignment: .leading, spacing: 18) {
HStack {
VStack(alignment: .leading, spacing: 4) {
Text("Reports")
Text("Advanced")
.font(.system(size: 36, weight: .black, design: .rounded))
Text("Raw artifacts remain here, away from the noob cockpit.")
Text("Raw artifacts and backend diagnostics live here, away from Mission Control.")
.foregroundStyle(.secondary)
}
Spacer()
@ -824,7 +882,7 @@ struct ReportsView: View {
GlassPanel {
VStack(alignment: .leading, spacing: 10) {
Label(lastCommand, systemImage: "terminal")
Label(lastCommand, systemImage: "wrench.and.screwdriver")
.font(.headline)
ScrollView {
Text(transcript)
@ -1095,37 +1153,35 @@ enum ResearchPhase {
struct Directive {
let title: String
let detail: String
let command: String?
let color: Color
static func from(_ appState: AppStatePayload?) -> Directive {
guard let appState else {
return Directive(title: "LOAD STATE", detail: "The app is reading the ratchet lifecycle database.", command: nil, color: .secondary)
return Directive(title: "LOAD STATE", detail: "The app is reading the ratchet lifecycle database.", color: .secondary)
}
if let watch = appState.operatorState.completedWatch {
return Directive(
title: "STOP",
detail: "Wait until \(watch.earliestActionLocal). Repeating the same watch now would be loop-chasing.",
command: "./scripts/ratchet once",
color: .orange
)
}
if appState.operatorState.activeWatch != nil {
return Directive(title: "WAIT", detail: "A watch is already running. Do not start another one.", command: nil, color: .orange)
return Directive(title: "WAIT", detail: "A watch is already running. Do not start another one.", color: .orange)
}
if !appState.operatorState.activeManualPositions.isEmpty {
return Directive(title: "HOLD", detail: "Manual Braiins exposure is active. Supervise it; do not start new experiments.", command: "./scripts/ratchet supervise --status", color: .orange)
return Directive(title: "HOLD", detail: "Manual Braiins exposure is active. Supervise it; do not start new experiments.", color: .orange)
}
if !appState.operatorState.hasOcean || !appState.operatorState.hasMarket || !appState.operatorState.isFresh {
return Directive(title: "REFRESH", detail: "The latest market state is stale or missing. Collect exactly one fresh sample.", command: "./scripts/ratchet once", color: .green)
return Directive(title: "REFRESH", detail: "The latest market state is stale or missing. Collect exactly one fresh sample.", color: .green)
}
if appState.operatorState.action == "manual_canary" {
return Directive(title: "WATCH", detail: "Run one bounded passive watch. This buys information, not a promise of profit.", command: "./scripts/ratchet watch 2", color: .green)
return Directive(title: "WATCH", detail: "Run one bounded passive watch. This buys information, not a promise of profit.", color: .green)
}
if appState.operatorState.action == "manual_bid" {
return Directive(title: "REVIEW", detail: "Read the full report before any manual Braiins action.", command: "./scripts/ratchet report", color: .green)
return Directive(title: "REVIEW", detail: "Read the full report before any manual Braiins action.", color: .green)
}
return Directive(title: "OBSERVE", detail: "No useful action window is visible right now.", command: nil, color: .secondary)
return Directive(title: "OBSERVE", detail: "No useful action window is visible right now.", color: .secondary)
}
}

View file

@ -45,6 +45,16 @@ class MacAppPackagingTest(unittest.TestCase):
self.assertIn("./scripts/ratchet app", text)
self.assertNotIn("swift run BraiinsRatchetMac", text)
def test_start_here_is_app_first_and_not_pipeline_first(self):
text = (ROOT / "START_HERE.md").read_text()
self.assertIn("This project now has one normal operator entry point", text)
self.assertIn("./scripts/ratchet app", text)
self.assertIn("The app is the control room", text)
self.assertIn("Who Is In Control", text)
self.assertNotIn("Controlled Automation", text)
self.assertNotIn("./scripts/ratchet pipeline", text)
def test_swift_app_uses_native_dashboard_not_raw_terminal_as_primary_ui(self):
source = ROOT / "macos" / "BraiinsRatchet" / "Sources" / "BraiinsRatchetMac" / "BraiinsRatchetApp.swift"
text = source.read_text()
@ -56,6 +66,11 @@ class MacAppPackagingTest(unittest.TestCase):
self.assertIn("AppStatePayload", text)
self.assertIn("loadAppState", text)
self.assertIn("PassiveRunCard", text)
self.assertIn("ControlOwnershipCard", text)
self.assertIn("EvidenceDeck", text)
self.assertIn("AdvancedView", text)
self.assertIn("Current Decision", text)
self.assertNotIn("Do This Now", text)
self.assertNotIn("Automation Gate", text)
self.assertNotIn("confirmationDialog", text)
self.assertNotIn("showAutomationApproval", text)