Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
a2e9f5c
docs(plan): mole-parity PRD (+ α Process Inspector, β Get Online comp…
caezium Jun 25, 2026
3d039a4
feat(parity): SecurityPosture + OSUpdateGate + RemovableVolumeGuard (…
caezium Jun 25, 2026
e7fb52f
feat(parity): 8 pure cores — clean-rank, sensitive-flag, update-seen,…
caezium Jun 25, 2026
5416e97
feat(parity): 8 more pure cores — BTM login items, pkgutil receipts, …
caezium Jun 25, 2026
caaa194
feat(parity): process export/filter, speed-test, nearby-networks, ele…
caezium Jun 25, 2026
43396e9
feat(doctor): security posture (SIP/Gatekeeper/FileVault/firewall) + …
caezium Jun 25, 2026
564e4a7
feat(startup): don't flag a LaunchAgent on an unplugged drive as broken
caezium Jun 25, 2026
f982d65
feat(clean): sort review by deletion impact + flag sensitive (keychai…
caezium Jun 25, 2026
c839443
feat(updates): hide App Store updates needing a newer macOS (OSUpdate…
caezium Jun 25, 2026
f3a8b3f
feat(analyze+awake): treemap 'Other' fold + lid-closed Keep Screen On
caezium Jun 26, 2026
bc2dce8
feat(updates+uninstall): ⌘R refresh + cache-bypass; alias-aware app s…
caezium Jun 26, 2026
b2ab873
feat(optimize): pre-run safety guard banner (VPN / external display a…
caezium Jun 26, 2026
6c765aa
feat(doctor): battery-health check (capacity %, omitted on desktops)
caezium Jun 26, 2026
3150c99
feat(clean): show all-time cleaned total on the done screen
caezium Jun 26, 2026
faab4a1
feat(startup): surface modern Login (BTM) items in the inventory
caezium Jun 26, 2026
01e7429
feat(uninstall): Clear-Data subset + input-method leftover warning
caezium Jun 26, 2026
f07cbdb
feat(status): suspend/resume + export for the process table (epic α)
caezium Jun 26, 2026
15de84f
feat(status): typed predicate filter over the process table (epic α)
caezium Jun 26, 2026
ca06666
fix(uninstall): add missing 'paths:' label on UninstallPlan.dataOnly
caezium Jun 26, 2026
cb426b0
feat(status): per-process inspector sheet (epic α)
caezium Jun 26, 2026
bae61bd
feat(status): per-process network in the inspector (epic α)
caezium Jun 26, 2026
c24c3a7
feat(status): process tree view (epic α)
caezium Jun 26, 2026
ce104e0
feat(status): per-process CPU watchdog engine (epic α, slice 1)
caezium Jun 26, 2026
32bb3b0
feat(settings): process watchdog editor (epic α, slice 2)
caezium Jun 26, 2026
ca97d98
feat(get-online): venue-specific captive-portal tips (epic β)
caezium Jun 26, 2026
697c0c4
feat(get-online): nearby Wi-Fi scan with channel congestion (epic β H…
caezium Jun 26, 2026
61d494d
feat(get-online): on-demand speed test (epic β)
caezium Jun 26, 2026
087febe
feat(get-online): connection history log (epic β)
caezium Jun 26, 2026
1a46094
feat(doctor): display / external-volume / network context (story 47)
caezium Jun 26, 2026
cd073e3
feat(status): deep per-process metrics in the inspector (epic α 56)
caezium Jun 26, 2026
71d0eec
feat(analyze): one-tap whole-disk scan (story 38)
caezium Jun 26, 2026
87e2b9e
feat(status): redesign process inspector as a structured panel (epic …
caezium Jun 26, 2026
cc90a9f
fix(ui): kill 3 main-thread hangs on the mole-parity surfaces
caezium Jun 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion macos/Sources/AnalyzeView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,12 @@ struct AnalyzeView: View {
}
Spacer()
Text(model.usageLine).font(Brand.mono(10)).foregroundStyle(Brand.textTertiary)
Button { model.scanWholeDisk() } label: {
Image(systemName: "externaldrive").font(.system(size: 11, weight: .semibold))
.foregroundStyle(model.crumbs.first?.path == "/" ? Brand.textTertiary.opacity(0.35) : Brand.textSecondary)
}
.buttonStyle(.plain).disabled(model.crumbs.first?.path == "/")
.help(NSLocalizedString("Scan the whole disk", comment: ""))
Button { model.refresh() } label: {
Image(systemName: "arrow.clockwise").font(.system(size: 11, weight: .semibold))
.foregroundStyle(Brand.textSecondary)
Expand All @@ -192,7 +198,17 @@ struct TreemapView: View {

var body: some View {
GeometryReader { geo in
let shown = Array(entries.filter { $0.size > 0 }.prefix(120))
// Largest 120 cells; the long tail folds into one inert "Other"
// cell so the map stays legible AND its total matches (PRD §Analyze).
let shown: [DiskScanEntry] = {
let sorted = entries.filter { $0.size > 0 }.sorted { $0.size > $1.size }
guard sorted.count > 120 else { return sorted }
let tail = sorted.dropFirst(120).reduce(Int64(0)) { $0 + $1.size }
return Array(sorted.prefix(120)) + (tail > 0
? [DiskScanEntry(id: "__other__", name: NSLocalizedString("Other", comment: ""),
path: "", size: tail, isDir: false, lastAccess: nil)]
: [])
}()
let rects = Treemap.layout(weights: shown.map { Double($0.size) },
in: CGRect(x: 0, y: 0, width: geo.size.width, height: geo.size.height))
// One immediate-mode draw pass, not 120 nested SwiftUI cells. The
Expand Down Expand Up @@ -364,6 +380,14 @@ final class AnalyzeModel: ObservableObject {
scan(last.path, name: last.name, push: false, force: true)
}

/// Re-root the scan at the whole disk (PRD §Analyze 38). Reachable by
/// climbing Up too, but this is the one-tap "show me everything" entry.
func scanWholeDisk() {
guard crumbs.first?.path != "/" else { return }
crumbs = []
scan("/", name: "/", push: true)
}

/// Whether there's a parent to climb to (Home isn't the ceiling — you can
/// go up to /Users, /, external volumes, …). False only at the filesystem root.
var canGoUp: Bool {
Expand Down
9 changes: 9 additions & 0 deletions macos/Sources/Awake.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ final class Awake: ObservableObject {

private var displayAssertion: IOPMAssertionID = 0
private var systemAssertion: IOPMAssertionID = 0
private var lidAssertion: IOPMAssertionID = 0
private var expiryTimer: Timer?

private init() {}
Expand All @@ -61,6 +62,13 @@ final class Awake: ObservableObject {
ok = IOPMAssertionCreateWithName(kIOPMAssertionTypePreventUserIdleSystemSleep as CFString,
IOPMAssertionLevel(kIOPMAssertionLevelOn),
reason, &systemAssertion) == kIOReturnSuccess && ok
// Opt-in (PRD §Everyday): also prevent system sleep so a backup/render
// survives a closed lid. Default off — it changes power behaviour.
if Store.keepAwakeLidClosed {
_ = IOPMAssertionCreateWithName(kIOPMAssertionTypePreventSystemSleep as CFString,
IOPMAssertionLevel(kIOPMAssertionLevelOn),
reason, &lidAssertion)
}
isActive = ok
if let secs = duration.seconds {
expiresAt = Date().addingTimeInterval(secs)
Expand All @@ -75,6 +83,7 @@ final class Awake: ObservableObject {
func stop() {
if displayAssertion != 0 { IOPMAssertionRelease(displayAssertion); displayAssertion = 0 }
if systemAssertion != 0 { IOPMAssertionRelease(systemAssertion); systemAssertion = 0 }
if lidAssertion != 0 { IOPMAssertionRelease(lidAssertion); lidAssertion = 0 }
expiryTimer?.invalidate()
expiryTimer = nil
isActive = false
Expand Down
22 changes: 22 additions & 0 deletions macos/Sources/BinaryIntegrity.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//
// BinaryIntegrity.swift
// Burrow
//
// Classifies whether a running process's executable on disk is intact, deleted,
// or replaced since launch (PRD §Status / §α — a security signal). Pure: feed
// the launch inode and the current on-disk inode at the same path; the
// proc_pidpath + stat reads are the seam.
//

import Foundation

enum BinaryIntegrity {
enum Verdict: String, Equatable { case intact, deleted, replaced }

/// `onDiskInode` nil = the path no longer exists (deleted/moved). A
/// different inode at the same path = the binary was replaced underneath it.
static func classify(launchInode: UInt64, onDiskInode: UInt64?) -> Verdict {
guard let now = onDiskInode else { return .deleted }
return now == launchInode ? .intact : .replaced
}
}
29 changes: 29 additions & 0 deletions macos/Sources/CleanImpactRanker.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//
// CleanImpactRanker.swift
// Burrow
//
// Orders clean review items by deletion impact (PRD §Clean): safest /
// most-regenerable first, user-visible state last. Pure — keyed off the
// category name the engine reports. Lower rank = safer = shown first.
//

import Foundation

enum CleanImpactRanker {
/// 0 = pure regenerable cache … 4 = credentials / user state.
static func rank(category: String) -> Int {
let c = category.lowercased()
if c.contains("credential") || c.contains("keychain") || c.contains("login") { return 4 }
if c.contains("document") || c.contains("state") || c.contains("essential") { return 3 }
if c.contains("log") || c.contains("leftover") || c.contains("trash") { return 2 }
if c.contains("download") || c.contains("derived") || c.contains("build") || c.contains("artifact") { return 1 }
return 0 // caches + everything else: safest
}

/// Stable ascending-impact sort, preserving input order within a rank.
static func sorted<T>(_ items: [(category: String, value: T)]) -> [T] {
items.enumerated()
.sorted { (rank(category: $0.element.category), $0.offset) < (rank(category: $1.element.category), $1.offset) }
.map { $0.element.value }
}
}
10 changes: 9 additions & 1 deletion macos/Sources/CleanReviewView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ struct CleanReviewView: View {
Rectangle().fill(Brand.hairline).frame(height: 1)
ScrollView {
LazyVStack(spacing: 10) {
ForEach(list.categories) { category in
// Safest / most-regenerable categories first (PRD §Clean).
ForEach(CleanImpactRanker.sorted(list.categories.map { (category: $0.name, value: $0) })) { category in
categoryCard(category)
}
}
Expand Down Expand Up @@ -215,6 +216,13 @@ struct CleanReviewView: View {
.lineLimit(1).truncationMode(.middle)
}
Spacer()
if SensitiveRemnantMatcher.isSensitive(item.path) {
Text(NSLocalizedString("sensitive", comment: ""))
.font(Brand.mono(9, .medium)).foregroundStyle(Brand.amber)
.padding(.horizontal, 5).padding(.vertical, 1.5)
.background(Capsule().fill(Brand.amber.opacity(0.16)))
.help(NSLocalizedString("Looks like a credential/keychain path — review before removing.", comment: ""))
}
badge(for: lockReason)
if let count = item.itemCount {
Text(String(format: NSLocalizedString("%d items", comment: ""), count))
Expand Down
22 changes: 20 additions & 2 deletions macos/Sources/CleanView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ struct CleanView: View {
/// Trash-mode result line, shown as a done banner.
@State private var trashResult: String?
@State private var fdaGranted = Privacy.hasFullDiskAccess()
/// All-time bytes cleaned (PRD §Clean), loaded off-main for the done screen.
@State private var lifetimeCleaned: Int64 = 0
@Environment(\.accessibilityReduceMotion) private var reduceMotion

var body: some View {
Expand Down Expand Up @@ -101,6 +103,22 @@ struct CleanView: View {
}
}

/// Done-screen detail: this run's freed line + the all-time total.
private var cleanedDetail: String? {
let thisRun = realFlow.report?.summary.map(\.completionLine)
let life = lifetimeCleaned > 0
? String(format: NSLocalizedString("Lifetime: %@ cleaned", comment: ""), Fmt.bytes(lifetimeCleaned))
: nil
let parts = [thisRun, life].compactMap { $0 }
return parts.isEmpty ? nil : parts.joined(separator: " · ")
}

private func loadLifetime() async {
lifetimeCleaned = await Task.detached(priority: .utility) {
CleanWatch.totals(from: MoleHistory.load()).cleanedBytes
}.value
}

private var dryRunFinished: Bool {
if case .finished = dryFlow.state { return true }
return false
Expand Down Expand Up @@ -361,8 +379,8 @@ struct CleanView: View {
.padding(.horizontal, 18).padding(.top, 4).padding(.bottom, 12)
Rectangle().fill(Brand.hairline).frame(height: 1)
if case .finished(.done) = realFlow.state {
DoneBanner(accent: Tool.clean.accent, title: "Cleaned",
detail: realFlow.report?.summary.map(\.completionLine))
DoneBanner(accent: Tool.clean.accent, title: "Cleaned", detail: cleanedDetail)
.task { await loadLifetime() }
}
TaskReportView(groups: realFlow.report?.groups ?? [], accent: Tool.clean.accent)
if case .finished = realFlow.state {
Expand Down
61 changes: 61 additions & 0 deletions macos/Sources/CodeSignInfo.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
//
// CodeSignInfo.swift
// Burrow
//
// Code-signing facts for the process inspector's Security section (PRD §α 55):
// signer, team id, hardened-runtime, app-sandbox, and signature validity —
// via the Security framework's SecCode APIs (unprivileged, by pid). All a
// syscall seam; there's no pure logic to unit-test here.
//

import Foundation
import Security

enum CodeSignInfo {
struct Info: Equatable {
let signer: String? // leaf-cert common name, else team id
let teamID: String?
let hardened: Bool
let sandboxed: Bool
let valid: Bool // signature validates against its designated requirement
}

// SecCSFlags raw values (CSCommon.h) — used numerically to avoid the
// constants' uneven Swift import.
private static let kSigningInfo: UInt32 = 0x2 | 0x4 // signing + requirement information
private static let kRuntimeFlag: UInt32 = 0x1_0000 // hardened runtime (CS_RUNTIME)

static func read(pid: Int) -> Info? {
let attrs = [kSecGuestAttributePid as String: NSNumber(value: pid)] as CFDictionary
var code: SecCode?
guard SecCodeCopyGuestWithAttributes(nil, attrs, [], &code) == errSecSuccess, let code else { return nil }
var stat: SecStaticCode?
guard SecCodeCopyStaticCode(code, [], &stat) == errSecSuccess, let stat else { return nil }

let valid = SecStaticCodeCheckValidity(stat, [], nil) == errSecSuccess

var infoCF: CFDictionary?
guard SecCodeCopySigningInformation(stat, SecCSFlags(rawValue: kSigningInfo), &infoCF) == errSecSuccess,
let info = infoCF as? [String: Any] else {
return Info(signer: nil, teamID: nil, hardened: false, sandboxed: false, valid: valid)
}

let teamID = info[kSecCodeInfoTeamIdentifier as String] as? String
let csFlags = (info[kSecCodeInfoFlags as String] as? NSNumber)?.uint32Value ?? 0
let hardened = (csFlags & kRuntimeFlag) != 0

var sandboxed = false
if let ent = info[kSecCodeInfoEntitlementsDict as String] as? [String: Any] {
sandboxed = (ent["com.apple.security.app-sandbox"] as? Bool) == true
}

var signer: String? = teamID
if let certs = info[kSecCodeInfoCertificates as String] as? [SecCertificate], let leaf = certs.first {
var cn: CFString?
if SecCertificateCopyCommonName(leaf, &cn) == errSecSuccess, let name = cn as String? {
signer = name
}
}
return Info(signer: signer, teamID: teamID, hardened: hardened, sandboxed: sandboxed, valid: valid)
}
}
24 changes: 24 additions & 0 deletions macos/Sources/ConnectionFailureClassifier.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//
// ConnectionFailureClassifier.swift
// Burrow
//
// Classifies why a Get Online attempt failed, from the probe verdicts, for the
// connection-history log (PRD §β). Pure.
//

import Foundation

enum ConnectionFailureClassifier {
enum Reason: String, Equatable {
case ok // online
case captivePortal // portal present, login page reachable
case loginUnreachable // portal present, login page itself didn't respond
case noInternet // no portal, no reachability
}

static func classify(online: Bool, portal: Bool, loginReachable: Bool) -> Reason {
if online { return .ok }
if portal { return loginReachable ? .captivePortal : .loginUnreachable }
return .noInternet
}
}
53 changes: 53 additions & 0 deletions macos/Sources/ConnectionHistory.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
//
// ConnectionHistory.swift
// Burrow
//
// Persisted log of Get-Online attempts (PRD §β): when, which network, and the
// classified outcome (ConnectionFailureClassifier). The append/cap/collapse
// core is pure + tested; the UserDefaults read/write is the seam.
//

import Foundation

enum ConnectionHistory {
struct Entry: Codable, Equatable {
let at: Date
let ssid: String?
let reason: String // ConnectionFailureClassifier.Reason.rawValue
}

static let cap = 50

/// Append newest-first, capped. A repeat of the most-recent (ssid, reason)
/// just refreshes that row's timestamp instead of growing the log — so
/// re-checking the same network doesn't spam identical rows.
static func appended(_ list: [Entry], _ e: Entry) -> [Entry] {
var out = list
if let first = out.first, first.ssid == e.ssid, first.reason == e.reason {
out[0] = e
} else {
out.insert(e, at: 0)
}
if out.count > cap { out = Array(out.prefix(cap)) }
return out
}

// MARK: - Store (UserDefaults JSON)

private static let key = "connection_history_v1"

static func load() -> [Entry] {
guard let data = UserDefaults.standard.data(forKey: key),
let list = try? JSONDecoder().decode([Entry].self, from: data) else { return [] }
return list
}

@discardableResult
static func record(ssid: String?, reason: String, at: Date) -> [Entry] {
let list = appended(load(), Entry(at: at, ssid: ssid, reason: reason))
if let data = try? JSONEncoder().encode(list) {
UserDefaults.standard.set(data, forKey: key)
}
return list
}
}
Loading
Loading