diff --git a/.gitignore b/.gitignore index 3541504..3aafb28 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ __pycache__/ data/*.sqlite data/*.sqlite-shm data/*.sqlite-wal +data/app_visual_state.* data/raw/ *.log macos/BraiinsRatchet/.build/ diff --git a/README.md b/README.md index 109dda1..77f7a78 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,19 @@ This builds `macos/build/Braiins Ratchet.app`, closes any stale `BraiinsRatchetM The app is a native Tahoe Flight Deck: animated hashfield background, real SwiftUI Liquid Glass controls, Hashflow, Ratchet, Bid Lab, Exposure, and Evidence. The design rationale is in `docs/APP_DESIGN_RESEARCH.md`. +The app also has a `Reality Mirror` self-reflection layer. It writes the exact semantic state the SwiftUI app believes it is showing to: + +```text +data/app_visual_state.md +data/app_visual_state.json +``` + +Print the latest mirror snapshot: + +```bash +./scripts/ratchet mirror +``` + Advanced fallback for a 6-hour CLI monitoring session: ```bash diff --git a/START_HERE.md b/START_HERE.md index 9bbd3de..c8001f5 100644 --- a/START_HERE.md +++ b/START_HERE.md @@ -76,6 +76,20 @@ The app is organized as: 4. `Bid Lab`: shadow order, expected net, breakeven, and loss boundary. 5. `Exposure`: record or close manually executed Braiins exposure. 6. `Evidence`: raw cockpit, report, and ledger artifacts for diagnostics. +7. `Reality Mirror`: self-reflective BED view that shows what the app believes it is rendering right now. + +The small `Reality Mirror` HUD writes the current visual/operator truth to: + +```text +data/app_visual_state.md +data/app_visual_state.json +``` + +If you ask for help, this file is the fastest way to show the exact app state instead of describing it from memory: + +```bash +./scripts/ratchet mirror +``` ## Research Pathway @@ -140,6 +154,7 @@ Use these only if the native app cannot be opened or you are debugging: ./scripts/ratchet position list ./scripts/ratchet report ./scripts/ratchet experiments +./scripts/ratchet mirror ./scripts/ratchet guide ./scripts/ratchet operator-guide ``` diff --git a/docs/CLI_REFERENCE.md b/docs/CLI_REFERENCE.md index e6b25f3..45fd9cb 100644 --- a/docs/CLI_REFERENCE.md +++ b/docs/CLI_REFERENCE.md @@ -17,6 +17,8 @@ Use `./scripts/ratchet guide` for the user-facing operating story. Use `./scripts/ratchet operator-guide` for architecture, installation, migration, backup, recovery, and diagnostics. +Use `./scripts/ratchet mirror` to print the native app's latest self-written visual-state snapshot from `data/app_visual_state.md`. + The raw Python CLI is documented below for debugging and development. All raw commands should be run from the repository root. @@ -50,6 +52,16 @@ These are wrapper commands, not raw Python subcommands: `operator-guide` prints `docs/OPERATOR_GUIDE.md`. +## Wrapper Reality Mirror Command + +This is a wrapper command, not a raw Python subcommand: + +```bash +./scripts/ratchet mirror +``` + +It prints `data/app_visual_state.md`, which is written by the SwiftUI app whenever it refreshes state or changes visible sections. If no snapshot exists yet, open the app with `./scripts/ratchet app` and refresh or open `Reality Mirror`. + ## `pipeline` Prints a monitor-only automation proposal and asks for `yes` or `no`. diff --git a/docs/OPERATOR_GUIDE.md b/docs/OPERATOR_GUIDE.md index 91db8ba..427e3c8 100644 --- a/docs/OPERATOR_GUIDE.md +++ b/docs/OPERATOR_GUIDE.md @@ -43,7 +43,8 @@ The current stack is: 6. SQLite durable state at `data/ratchet.sqlite`. 7. Markdown evidence reports under `reports/`. 8. Bash launchers in `scripts/`. -9. Git and GitHub on branch `master`. +9. SwiftUI self-reflection snapshots at `data/app_visual_state.md` and `data/app_visual_state.json`. +10. Git and GitHub on branch `master`. ### Runtime Layers @@ -71,6 +72,10 @@ Layer 6 is the background engine. `src/braiins_ratchet/engine.py` starts a detached monitor-only supervisor, writes `data/supervisor.pid`, and logs to `logs/supervisor.log`. +Layer 7 is the visual self-reflection layer. + +The SwiftUI app renders a `Reality Mirror` HUD and tab. It writes the semantic state it believes it is showing to `data/app_visual_state.md` and `data/app_visual_state.json`. This is not screenshot OCR; it is the app's own rendered-state ledger. + ### Data Flow Normal data flow: @@ -105,8 +110,10 @@ Operational files: 1. `data/supervisor.pid`: PID for the background engine. 2. `logs/supervisor.log`: background engine output. 3. `reports/ACTIVE_WATCH.json`: marker for a running watch. -4. `.venv`: local Python environment, disposable. -5. `macos/build/Braiins Ratchet.app`: generated app bundle, disposable. +4. `data/app_visual_state.md`: latest human-readable visual self-reflection snapshot. +5. `data/app_visual_state.json`: latest machine-readable visual self-reflection snapshot. +6. `.venv`: local Python environment, disposable. +7. `macos/build/Braiins Ratchet.app`: generated app bundle, disposable. Source files: @@ -559,6 +566,12 @@ Check app state: ./scripts/ratchet app-state ``` +Check what the visible app surface believes it is showing: + +```bash +./scripts/ratchet mirror +``` + Check latest plain report: ```bash diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md index 891c1e3..950e6cf 100644 --- a/docs/USER_GUIDE.md +++ b/docs/USER_GUIDE.md @@ -73,9 +73,44 @@ The normal app tabs are: 4. `Bid Lab`: shadow order, breakeven, expected net, and loss budget. 5. `Exposure`: manual Braiins position recorder. 6. `Evidence`: raw reports and diagnostic text. +7. `Reality Mirror`: the app's self-reflective BED view. If you are unsure, stay on `Flight Deck`. +## Reality Mirror + +The `Reality Mirror` is the app looking at itself. + +BED means `Backstage Evidence Deck`. + +It exists because generic advice is not enough. The app writes what it believes it is rendering right now: + +1. Visible section. +2. Giant decision word. +3. Control owner. +4. Next action. +5. Engine state. +6. Strategy action. +7. Braiins freshness. +8. Active watch, cooldown, or manual exposure. +9. Buttons it believes are visible. +10. Operator truths for the current state. + +The files are: + +```text +data/app_visual_state.md +data/app_visual_state.json +``` + +If you ask for help, run: + +```bash +./scripts/ratchet mirror +``` + +That prints the latest self-written visual state, so the helper can reason about what is actually on your app surface. + ## Flight Deck Reading Order Read the screen in this order every time: @@ -333,8 +368,9 @@ Use this order: 2. `Ratchet`: where the experiment is in the loop. 3. `Bid Lab`: why the current proposal exists. 4. `Exposure`: whether real money blocks new experiments. -5. `Evidence`: raw report and ledger. -6. `docs/OPERATOR_GUIDE.md`: installation, recovery, migration, and diagnostics. +5. `Reality Mirror`: what the app believes it is showing right now. +6. `Evidence`: raw report and ledger. +7. `docs/OPERATOR_GUIDE.md`: installation, recovery, migration, and diagnostics. ## Glossary diff --git a/macos/BraiinsRatchet/README.md b/macos/BraiinsRatchet/README.md index 3a47aec..84aea90 100644 --- a/macos/BraiinsRatchet/README.md +++ b/macos/BraiinsRatchet/README.md @@ -22,6 +22,7 @@ This builds `macos/build/Braiins Ratchet.app`, closes any stale `BraiinsRatchetM - Forever Engine controls for the monitor-only background lifecycle. - Manual exposure recording and closing controls. - Evidence view for raw artifacts and backend diagnostics. +- Reality Mirror BED layer plus `data/app_visual_state.md/json` self-reflection snapshots. - Monitor-only. It never places Braiins orders. ## Product Direction diff --git a/macos/BraiinsRatchet/Sources/BraiinsRatchetMac/BraiinsRatchetApp.swift b/macos/BraiinsRatchet/Sources/BraiinsRatchetMac/BraiinsRatchetApp.swift index 8e01393..7ab5549 100644 --- a/macos/BraiinsRatchet/Sources/BraiinsRatchetMac/BraiinsRatchetApp.swift +++ b/macos/BraiinsRatchet/Sources/BraiinsRatchetMac/BraiinsRatchetApp.swift @@ -112,6 +112,17 @@ final class RatchetStore: ObservableObject { closePositionId = "" } + func writeRealitySnapshot(section: AppSection?) { + let reality = RenderedReality.make( + section: section ?? .deck, + appState: appState, + isWorking: isWorking, + operation: operation, + errorMessage: errorMessage + ) + RealitySnapshotWriter.write(reality) + } + private func run(label: String, arguments: [String], refreshAfterwards: Bool) async { operation = label rawTitle = label @@ -135,12 +146,22 @@ struct FlightDeckApp: View { root .task { await store.refresh() + store.writeRealitySnapshot(section: selection) } .onAppear { withAnimation(.easeInOut(duration: 4.8).repeatForever(autoreverses: true)) { pulse = true } } + .onChange(of: selection) { _, newValue in + store.writeRealitySnapshot(section: newValue) + } + .onChange(of: store.appState?.generatedAt) { _, _ in + store.writeRealitySnapshot(section: selection) + } + .onChange(of: store.operation) { _, _ in + store.writeRealitySnapshot(section: selection) + } } private var root: some View { @@ -158,6 +179,17 @@ struct FlightDeckApp: View { selectedView .safeAreaPadding(.horizontal, 30) .safeAreaPadding(.vertical, 24) + VStack { + Spacer() + HStack { + Spacer() + RealityHUD(store: store, section: selection ?? .deck) { + selection = .mirror + } + .padding(.trailing, 24) + .padding(.bottom, 20) + } + } } .toolbar { toolbarContent } .searchable(text: $store.query, placement: .toolbar, prompt: "Search reports, prices, OCEAN, Braiins") @@ -218,6 +250,8 @@ struct FlightDeckApp: View { ExposureView(store: store) case .vault: EvidenceVaultView(store: store) + case .mirror: + RealityMirrorView(store: store, section: selection ?? .mirror) } } } @@ -229,6 +263,7 @@ enum AppSection: String, CaseIterable, Identifiable { case bidlab case exposure case vault + case mirror var id: String { rawValue } @@ -240,6 +275,7 @@ enum AppSection: String, CaseIterable, Identifiable { case .bidlab: "Bid Lab" case .exposure: "Exposure" case .vault: "Evidence" + case .mirror: "Reality Mirror" } } @@ -251,6 +287,7 @@ enum AppSection: String, CaseIterable, Identifiable { case .bidlab: "shadow order optics" case .exposure: "manual position lock" case .vault: "reports and raw state" + case .mirror: "BED: app sees itself" } } @@ -262,6 +299,7 @@ enum AppSection: String, CaseIterable, Identifiable { case .bidlab: "slider.horizontal.3" case .exposure: "lock.shield" case .vault: "archivebox" + case .mirror: "eye.square" } } } @@ -1089,6 +1127,156 @@ struct EvidenceVaultView: View { } } +struct RealityHUD: View { + @ObservedObject var store: RatchetStore + let section: AppSection + let openMirror: () -> Void + + private var reality: RenderedReality { + RenderedReality.make( + section: section, + appState: store.appState, + isWorking: store.isWorking, + operation: store.operation, + errorMessage: store.errorMessage + ) + } + + var body: some View { + LiquidGlassSurface(tint: .white.opacity(0.10), cornerRadius: 24) { + VStack(alignment: .leading, spacing: 10) { + HStack { + Label("Reality Mirror", systemImage: "eye") + .font(.caption.weight(.heavy)) + .textCase(.uppercase) + .foregroundStyle(.secondary) + Spacer() + Button("Open") { + openMirror() + } + .buttonStyle(.glass) + .controlSize(.small) + } + Text(reality.decisionTitle) + .font(.title2.weight(.black)) + .foregroundStyle(reality.decisionTint) + Text(reality.currentInstruction) + .font(.caption) + .foregroundStyle(.secondary) + .lineLimit(3) + Text("writes data/app_visual_state.md") + .font(.caption2.monospaced()) + .foregroundStyle(.tertiary) + } + .padding(14) + .frame(width: 330, alignment: .leading) + } + .accessibilityLabel("Reality Mirror heads-up display, current decision \(reality.decisionTitle)") + } +} + +struct RealityMirrorView: View { + @ObservedObject var store: RatchetStore + let section: AppSection + + private var reality: RenderedReality { + RenderedReality.make( + section: section, + appState: store.appState, + isWorking: store.isWorking, + operation: store.operation, + errorMessage: store.errorMessage + ) + } + + var body: some View { + ScrollView { + VStack(alignment: .leading, spacing: 22) { + SectionHeader("Reality Mirror", "BED: Backstage Evidence Deck. This is the app reflecting the concrete state it is rendering now, not generic advice.") + + HStack(alignment: .top, spacing: 18) { + LiquidGlassSurface(tint: reality.decisionTint.opacity(0.22), cornerRadius: 38) { + VStack(alignment: .leading, spacing: 14) { + Label("What I am showing", systemImage: "eye.fill") + .font(.title2.weight(.black)) + Text(reality.decisionTitle) + .font(.system(size: 56, weight: .black, design: .rounded)) + .foregroundStyle(reality.decisionTint) + Text(reality.decisionExplanation) + .font(.title3.weight(.semibold)) + .fixedSize(horizontal: false, vertical: true) + Text(reality.currentInstruction) + .font(.callout) + .foregroundStyle(.secondary) + } + .padding(24) + } + + LiquidGlassSurface(tint: .cyan.opacity(0.16), cornerRadius: 38) { + VStack(alignment: .leading, spacing: 14) { + Label("Snapshot artifact", systemImage: "doc.text.magnifyingglass") + .font(.title2.weight(.black)) + Text("The SwiftUI app writes exactly this semantic view state into the repo.") + .font(.callout) + .foregroundStyle(.secondary) + Button { + store.writeRealitySnapshot(section: section) + } label: { + Label("Write Snapshot Now", systemImage: "square.and.arrow.down") + } + .buttonStyle(.glassProminent) + .tint(.cyan) + Text("data/app_visual_state.md\n data/app_visual_state.json") + .font(.body.monospaced()) + .foregroundStyle(.secondary) + .textSelection(.enabled) + } + .padding(24) + } + } + + LiquidGlassSurface(tint: .white.opacity(0.08), cornerRadius: 34) { + VStack(alignment: .leading, spacing: 14) { + Label("Current visible facts", systemImage: "list.bullet.rectangle") + .font(.title2.weight(.black)) + ValueGrid(rows: reality.factGridRows) + } + .padding(22) + } + + LiquidGlassSurface(tint: .green.opacity(0.12), cornerRadius: 34) { + VStack(alignment: .leading, spacing: 14) { + Label("Buttons I believe are visible", systemImage: "cursorarrow.click.2") + .font(.title2.weight(.black)) + ForEach(reality.visibleButtons, id: \.self) { button in + Text(button) + .font(.body.monospaced()) + .textSelection(.enabled) + } + } + .padding(22) + } + + LiquidGlassSurface(tint: .orange.opacity(0.12), cornerRadius: 34) { + VStack(alignment: .leading, spacing: 14) { + Label("Operator truth", systemImage: "exclamationmark.shield") + .font(.title2.weight(.black)) + ForEach(reality.operatorTruths, id: \.self) { truth in + Text(truth) + .font(.callout) + .fixedSize(horizontal: false, vertical: true) + } + } + .padding(22) + } + } + } + .onAppear { + store.writeRealitySnapshot(section: section) + } + } +} + struct SectionHeader: View { let title: String let subtitle: String @@ -1323,6 +1511,305 @@ enum ControlOwner { } } +struct RealityRow: Codable, Hashable { + let label: String + let value: String + let unit: String +} + +struct RenderedReality: Codable { + let snapshotGeneratedAt: String + let source: String + let visibleSection: String + let visibleSectionSubtitle: String + let decisionTitle: String + let decisionExplanation: String + let controlTitle: String + let controlDetail: String + let nextTitle: String + let nextDetail: String + let currentInstruction: String + let engineRunning: Bool + let engineDetail: String + let operation: String + let errorMessage: String + let latestStrategyAction: String + let latestReport: String + let activeWatch: String + let completedWatch: String + let activeManualExposure: String + let braiinsFreshness: String + let latestOceanSample: String + let latestBraiinsSample: String + let instrumentRows: [RealityRow] + let visibleButtons: [String] + let operatorTruths: [String] + + var factRows: [RealityRow] { + [ + RealityRow(label: "visible section", value: visibleSection, unit: "screen"), + RealityRow(label: "decision", value: decisionTitle, unit: "giant word"), + RealityRow(label: "control", value: controlTitle, unit: "owner"), + RealityRow(label: "next", value: nextTitle, unit: "action"), + RealityRow(label: "engine", value: engineRunning ? "running" : "stopped", unit: "state"), + RealityRow(label: "strategy", value: latestStrategyAction, unit: "proposal"), + RealityRow(label: "braiins", value: braiinsFreshness, unit: "freshness"), + RealityRow(label: "manual exposure", value: activeManualExposure, unit: "blocker"), + ] + instrumentRows + } + + var factGridRows: [(String, String, String)] { + factRows.map { ($0.label, $0.value, $0.unit) } + } + + var decisionTint: Color { + switch decisionTitle { + case "ENGINE LIVE", "REFRESH", "WATCH", "REVIEW": + .green + case "WORKING", "HOLD", "WAIT", "COOLDOWN": + .orange + default: + .secondary + } + } + + static func make( + section: AppSection, + appState: AppStatePayload?, + isWorking: Bool, + operation: String?, + errorMessage: String? + ) -> RenderedReality { + let decision = Decision.from(appState, isWorking: isWorking) + let control = ControlOwner.from(appState, isWorking: isWorking) + let next = nextAction(appState) + let latest = appState?.latest + let operatorState = appState?.operatorState + let engineStatus = appState?.engineStatus + let activePositions = operatorState?.activeManualPositions ?? [] + let completedWatch = operatorState?.completedWatch + let buttons = visibleButtons(section: section, appState: appState, isWorking: isWorking) + let truths = operatorTruths( + decision: decision, + appState: appState, + activePositions: activePositions, + completedWatch: completedWatch + ) + + return RenderedReality( + snapshotGeneratedAt: ISO8601DateFormatter().string(from: Date()), + source: "SwiftUI rendered semantic state; this is not a screenshot or generic documentation.", + visibleSection: section.title, + visibleSectionSubtitle: section.subtitle, + decisionTitle: decision.title, + decisionExplanation: decision.explanation, + controlTitle: control.title, + controlDetail: control.detail, + nextTitle: next.title, + nextDetail: next.detail, + currentInstruction: next.instruction, + engineRunning: engineStatus?.running == true, + engineDetail: engineStatus?.detail ?? "engine state not loaded", + operation: operation ?? "none", + errorMessage: errorMessage ?? "none", + latestStrategyAction: operatorState?.action ?? "none", + latestReport: operatorState?.latestReport ?? "none", + activeWatch: operatorState?.activeWatch ?? "none", + completedWatch: completedWatch.map { "\($0.reportPath), remaining \($0.remainingMinutes)m, earliest \($0.earliestActionLocal)" } ?? "none", + activeManualExposure: activePositions.isEmpty ? "none" : activePositions.joined(separator: "; "), + braiinsFreshness: freshnessText(operatorState), + latestOceanSample: operatorState?.latestOceanTimestamp ?? "none", + latestBraiinsSample: operatorState?.latestMarketTimestamp ?? "none", + instrumentRows: [ + RealityRow(label: "braiins price", value: marketValue(latest, "fillable_price_btc_per_eh_day", fallback: marketValue(latest, "best_ask_btc_per_eh_day")), unit: "BTC/EH/day"), + RealityRow(label: "ocean hashrate", value: oceanValue(latest, "pool_hashrate_eh_s"), unit: "EH/s"), + RealityRow(label: "pool window", value: oceanValue(latest, "avg_block_time_hours"), unit: "h/block"), + RealityRow(label: "expected net", value: sats(proposalValue(latest, "expected_net_btc")), unit: "model"), + ], + visibleButtons: buttons, + operatorTruths: truths + ) + } + + private static func nextAction(_ appState: AppStatePayload?) -> (title: String, detail: String, instruction: String) { + guard let state = appState else { + return ("Load state", "Reading durable SQLite state.", "Wait for the app-state load to finish.") + } + if state.engineStatus.running { + return ( + "Engine owns it", + "No babysitting. The background engine waits, samples, watches, and writes evidence.", + "Do nothing unless you intentionally want to stop the engine or record manual exposure." + ) + } + if state.operatorState.activeWatch != nil { + return ( + "Wait for watch", + "A passive watch is already running.", + "Do not start another watch. Wait for the current run report." + ) + } + if let watch = state.operatorState.completedWatch { + return ( + "Wait \(watch.remainingMinutes)m", + "Earliest useful action: \(watch.earliestActionLocal).", + "Cooldown is active. Do not repeat the same experiment yet." + ) + } + if !state.operatorState.activeManualPositions.isEmpty { + return ( + "Hold exposure", + "A manually recorded Braiins position blocks new experiments.", + "Supervise the real position. Close it only when it is truly finished." + ) + } + let step = state.automationPlan.steps.first ?? "No passive action is useful right now." + return (state.automationPlan.title, step, step) + } + + private static func visibleButtons(section: AppSection, appState: AppStatePayload?, isWorking: Bool) -> [String] { + var buttons = ["Toolbar: Refresh"] + if appState?.engineStatus.running == true { + buttons.append("Toolbar: Stop Engine") + } else { + buttons.append("Toolbar: Start Engine") + } + switch section { + case .deck: + buttons.append(contentsOf: ["Flight Deck: Start Forever Engine", "Flight Deck: Stop", "Flight Deck: One Sample"]) + case .exposure: + buttons.append(contentsOf: ["Exposure: Record Exposure", "Exposure: Close Exposure"]) + case .vault: + buttons.append(contentsOf: ["Evidence: Cockpit", "Evidence: Report", "Evidence: Ledger"]) + case .mirror: + buttons.append("Reality Mirror: Write Snapshot Now") + default: + break + } + if isWorking { + buttons.append("State: some buttons are disabled because an operation is running") + } + return buttons + } + + private static func operatorTruths( + decision: Decision, + appState: AppStatePayload?, + activePositions: [String], + completedWatch: CompletedWatchPayload? + ) -> [String] { + if appState == nil { + return ["The app has not loaded structured backend state yet."] + } + if !activePositions.isEmpty { + return [ + "Real manual exposure is recorded.", + "New watch experiments should remain blocked until the exposure is closed." + ] + } + if appState?.engineStatus.running == true { + return [ + "The forever engine owns passive research.", + "The safest operator workload is zero unless you need to record real exposure." + ] + } + if let completedWatch { + return [ + "A watch already produced evidence.", + "Earliest useful next action is \(completedWatch.earliestActionLocal)." + ] + } + switch decision { + case .watch: + return ["A passive watch or the forever engine is the current research action; no BTC is spent by the app."] + case .review: + return ["Manual review is required before any Braiins action; the app still cannot spend BTC."] + case .observe: + return ["No useful action window is visible; doing nothing is the intended action."] + default: + return [decision.explanation] + } + } + + private static func freshnessText(_ state: OperatorStatePayload?) -> String { + guard let minutes = state?.freshnessMinutes else { return "unknown" } + return minutes <= 30 ? "fresh (\(minutes)m)" : "stale (\(minutes)m)" + } + + private static func marketValue(_ latest: LatestPayload?, _ key: String, fallback: String = "n/a") -> String { + latest?.market?[key]?.description ?? fallback + } + + private static func oceanValue(_ latest: LatestPayload?, _ key: String) -> String { + latest?.ocean?[key]?.description ?? "n/a" + } + + private static func proposalValue(_ latest: LatestPayload?, _ key: String) -> String { + latest?.proposal?[key]?.description ?? "n/a" + } +} + +enum RealitySnapshotWriter { + static func write(_ reality: RenderedReality) { + guard let repoRoot = RatchetProcess.repoRootURL() else { return } + let dataDir = repoRoot.appendingPathComponent("data", isDirectory: true) + do { + try FileManager.default.createDirectory(at: dataDir, withIntermediateDirectories: true) + let encoder = JSONEncoder() + encoder.outputFormatting = [.prettyPrinted, .sortedKeys] + let jsonData = try encoder.encode(reality) + try jsonData.write(to: dataDir.appendingPathComponent("app_visual_state.json"), options: .atomic) + try renderMarkdown(reality).write( + to: dataDir.appendingPathComponent("app_visual_state.md"), + atomically: true, + encoding: .utf8 + ) + } catch { + // This is diagnostic output only. UI operation must never fail because the mirror file cannot be written. + } + } + + private static func renderMarkdown(_ reality: RenderedReality) -> String { + var lines = [ + "# App Visual State", + "", + "This file is written by the SwiftUI app. It records the app's semantic rendered state, not generic documentation.", + "", + "- snapshot_generated_at: \(reality.snapshotGeneratedAt)", + "- visible_section: \(reality.visibleSection)", + "- decision: \(reality.decisionTitle)", + "- decision_explanation: \(reality.decisionExplanation)", + "- control: \(reality.controlTitle)", + "- control_detail: \(reality.controlDetail)", + "- next: \(reality.nextTitle)", + "- next_detail: \(reality.nextDetail)", + "- current_instruction: \(reality.currentInstruction)", + "- engine_running: \(reality.engineRunning ? "yes" : "no")", + "- engine_detail: \(reality.engineDetail)", + "- operation: \(reality.operation)", + "- error_message: \(reality.errorMessage)", + "- latest_strategy_action: \(reality.latestStrategyAction)", + "- braiins_freshness: \(reality.braiinsFreshness)", + "- latest_ocean_sample: \(reality.latestOceanSample)", + "- latest_braiins_sample: \(reality.latestBraiinsSample)", + "- latest_report: \(reality.latestReport)", + "- active_watch: \(reality.activeWatch)", + "- completed_watch: \(reality.completedWatch)", + "- active_manual_exposure: \(reality.activeManualExposure)", + "", + "## Instruments", + "", + ] + lines.append(contentsOf: reality.instrumentRows.map { "- \($0.label): \($0.value) \($0.unit)" }) + lines.append(contentsOf: ["", "## Visible Buttons", ""]) + lines.append(contentsOf: reality.visibleButtons.map { "- \($0)" }) + lines.append(contentsOf: ["", "## Operator Truths", ""]) + lines.append(contentsOf: reality.operatorTruths.map { "- \($0)" }) + lines.append("") + return lines.joined(separator: "\n") + } +} + enum ResearchPhase { case loading case refresh @@ -1561,6 +2048,10 @@ enum AppIconFactory { } enum RatchetProcess { + static func repoRootURL() -> URL? { + findRepoRoot() + } + static func loadAppState() async -> AppStateLoadResult { await Task.detached { guard let repoRoot = findRepoRoot() else { diff --git a/scripts/ratchet b/scripts/ratchet index 4471420..ee5697f 100755 --- a/scripts/ratchet +++ b/scripts/ratchet @@ -16,6 +16,7 @@ Commands: supervise Run the durable forever lifecycle supervisor. engine Start/stop/status for the background monitor engine. app Build and open the native macOS app. + mirror Print the app's latest self-written visual-state snapshot. position Record/list/close manually executed Braiins exposure. report Print the latest stored report without fetching new data. app-state Print structured JSON for the native macOS app. @@ -36,6 +37,7 @@ Examples: ./scripts/ratchet supervise ./scripts/ratchet engine status ./scripts/ratchet app + ./scripts/ratchet mirror ./scripts/ratchet position list ./scripts/ratchet report ./scripts/ratchet experiments @@ -142,6 +144,17 @@ cmd_app() { open "$app_path" } +cmd_mirror() { + local path="$ROOT_DIR/data/app_visual_state.md" + if [[ ! -f "$path" ]]; then + echo "No app visual-state snapshot exists yet." + echo "Open the native app with: ./scripts/ratchet app" + echo "Then refresh or open the Reality Mirror tab." + return 0 + fi + cat "$path" +} + cmd_position() { run_python -m braiins_ratchet.cli position "$@" } @@ -194,6 +207,7 @@ main() { supervise|daemon) cmd_supervise "$@" ;; engine) cmd_engine "$@" ;; app|mac-app) cmd_app "$@" ;; + mirror|reality) cmd_mirror "$@" ;; position|positions) cmd_position "$@" ;; report) cmd_report "$@" ;; app-state) cmd_app_state "$@" ;; diff --git a/tests/test_docs.py b/tests/test_docs.py index bf39b92..c03d67f 100644 --- a/tests/test_docs.py +++ b/tests/test_docs.py @@ -19,6 +19,9 @@ class DocumentationContractTests(unittest.TestCase): self.assertIn("Immediate", text) self.assertIn("Midterm", text) self.assertIn("Longterm", text) + self.assertIn("Reality Mirror", text) + self.assertIn("Backstage Evidence Deck", text) + self.assertIn("data/app_visual_state.md", text) self.assertIn("Potential Findings", text) self.assertIn("manual_canary", text) self.assertIn("manual_bid", text) @@ -37,6 +40,8 @@ class DocumentationContractTests(unittest.TestCase): self.assertIn("SQLite durable state", text) self.assertIn("Switching To Another macOS Host", text) self.assertIn("State Recovery", text) + self.assertIn("visual self-reflection layer", text) + self.assertIn("data/app_visual_state.json", text) self.assertIn("data/ratchet.sqlite*", text) self.assertIn("reports/EXPERIMENT_LOG.md", text) self.assertIn("git clone -b master", text) @@ -51,10 +56,13 @@ class DocumentationContractTests(unittest.TestCase): self.assertIn("docs/OPERATOR_GUIDE.md", readme) self.assertIn("./scripts/ratchet guide", readme) self.assertIn("./scripts/ratchet operator-guide", readme) + self.assertIn("./scripts/ratchet mirror", readme) self.assertIn("./scripts/ratchet guide", start_here) self.assertIn("./scripts/ratchet operator-guide", start_here) + self.assertIn("./scripts/ratchet mirror", start_here) self.assertIn("guide|user-guide|explain", wrapper) self.assertIn("operator-guide|operator", wrapper) + self.assertIn("mirror|reality", wrapper) if __name__ == "__main__": diff --git a/tests/test_mac_app.py b/tests/test_mac_app.py index eea8da4..b647ca3 100644 --- a/tests/test_mac_app.py +++ b/tests/test_mac_app.py @@ -16,6 +16,8 @@ class MacAppPackagingTest(unittest.TestCase): self.assertIn("cmd_app", text) self.assertIn("app-state", text) self.assertIn("engine", text) + self.assertIn("mirror|reality", text) + self.assertIn("app_visual_state.md", text) self.assertIn("pkill -x BraiinsRatchetMac", text) self.assertNotIn("swift run BraiinsRatchetMac", text) @@ -87,9 +89,14 @@ class MacAppPackagingTest(unittest.TestCase): self.assertIn("RatchetMapView", text) self.assertIn("BidLabView", text) self.assertIn("EvidenceVaultView", text) + self.assertIn("RealityMirrorView", text) + self.assertIn("RealityHUD", text) + self.assertIn("RenderedReality", text) + self.assertIn("RealitySnapshotWriter", text) self.assertIn("AppStatePayload", text) self.assertIn("EngineStatusPayload", text) self.assertIn("loadAppState", text) + self.assertIn("repoRootURL", text) self.assertIn("Start Forever Engine", text) self.assertIn("glassEffect", text) self.assertIn("GlassEffectContainer", text) @@ -97,6 +104,10 @@ class MacAppPackagingTest(unittest.TestCase): self.assertIn("backgroundExtensionEffect", text) self.assertIn("searchable", text) self.assertIn("Flight Deck", text) + self.assertIn("Reality Mirror", text) + self.assertIn("BED: Backstage Evidence Deck", text) + self.assertIn("app_visual_state.md", text) + self.assertIn("app_visual_state.json", text) self.assertIn("Bid Lab", text) self.assertNotIn("MissionControlView", text) self.assertNotIn("MiningStackView", text)